aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMSQLite.m
diff options
context:
space:
mode:
authorGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:35:12 +0000
committerGravatar thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2009-02-26 01:35:12 +0000
commitfde1ea051c0385638accd67362cd25f2fe1ae654 (patch)
treea43b02c31f73aa8a83e20abf619c02ec25030e32 /Foundation/GTMSQLite.m
parent93796b6367645ea20b01649c4e397a95370ed727 (diff)
adding SQLite helper/wrapper
Diffstat (limited to 'Foundation/GTMSQLite.m')
-rw-r--r--Foundation/GTMSQLite.m2013
1 files changed, 2013 insertions, 0 deletions
diff --git a/Foundation/GTMSQLite.m b/Foundation/GTMSQLite.m
new file mode 100644
index 0000000..01e3bad
--- /dev/null
+++ b/Foundation/GTMSQLite.m
@@ -0,0 +1,2013 @@
+//
+// GTMSQLite.m
+//
+// Convenience wrapper for SQLite storage see the header for details.
+//
+// Copyright 2007-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+
+#import <Foundation/Foundation.h>
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+#import <dlfcn.h>
+#endif
+#import "GTMSQLite.h"
+#import "GTMMethodCheck.h"
+#import "GTMDefines.h"
+#include <limits.h>
+#import "GTMGarbageCollection.h"
+
+typedef struct {
+ BOOL upperCase;
+ int textRep;
+} UpperLowerUserArgs;
+
+typedef struct {
+ BOOL reverse;
+ CFOptionFlags compareOptions;
+ int textRep;
+} CollateUserArgs;
+
+typedef struct {
+ CFOptionFlags *compareOptionPtr;
+ int textRep;
+} LikeGlobUserArgs;
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+// While we want to be compatible with Tiger, some operations are more
+// efficient when implemented with Leopard APIs. We look those up dynamically.
+// CFStringCreateWithBytesNoCopy
+static const char* const kCFStringCreateWithBytesNoCopySymbolName =
+ "CFStringCreateWithBytesNoCopy";
+
+typedef CFStringRef (*CFStringCreateWithBytesNoCopyPtrType)(CFAllocatorRef,
+ const UInt8 *,
+ CFIndex,
+ CFStringEncoding,
+ Boolean,
+ CFAllocatorRef);
+static CFStringCreateWithBytesNoCopyPtrType gCFStringCreateWithBytesNoCopySymbol = NULL;
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+// Helper inline for SQLite text type to CF endcoding
+GTM_INLINE CFStringEncoding SqliteTextEncodingToCFStringEncoding(int enc) {
+ // Default should never happen, but assume UTF 8
+ CFStringEncoding encoding = kCFStringEncodingUTF8;
+ _GTMDevAssert(enc == SQLITE_UTF16BE ||
+ enc == SQLITE_UTF16LE,
+ @"Passed in encoding was not a UTF16 encoding");
+ switch(enc) {
+ case SQLITE_UTF16BE:
+ encoding = kCFStringEncodingUTF16BE;
+ break;
+ case SQLITE_UTF16LE:
+ encoding = kCFStringEncodingUTF16LE;
+ break;
+ }
+ return encoding;
+}
+
+// Helper inline for filtering CFStringCompareFlags
+GTM_INLINE CFOptionFlags FilteredStringCompareFlags(CFOptionFlags inOptions) {
+ CFOptionFlags outOptions = 0;
+ if (inOptions & kCFCompareCaseInsensitive) {
+ outOptions |= kCFCompareCaseInsensitive;
+ }
+ if (inOptions & kCFCompareNonliteral) outOptions |= kCFCompareNonliteral;
+ if (inOptions & kCFCompareLocalized) outOptions |= kCFCompareLocalized;
+ if (inOptions & kCFCompareNumerically) outOptions |= kCFCompareNumerically;
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+ if (inOptions & kCFCompareDiacriticInsensitive) {
+ outOptions |= kCFCompareDiacriticInsensitive;
+ }
+ if (inOptions & kCFCompareWidthInsensitive) {
+ outOptions |= kCFCompareWidthInsensitive;
+ }
+#endif
+ return outOptions;
+}
+
+// Function prototypes for our custom implementations of UPPER/LOWER using
+// CFString so that we handle Unicode and localization more cleanly than
+// native SQLite.
+static void UpperLower8(sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv);
+static void UpperLower16(sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv);
+
+// Function prototypes for CFString-based collation sequences
+static void CollateNeeded(void *userContext, sqlite3 *db,
+ int textRep, const char *name);
+static int Collate8(void *userContext, int length1, const void *str1,
+ int length2, const void *str2);
+static int Collate16(void *userContext, int length1, const void *str1,
+ int length2, const void *str2);
+
+// Function prototypes for CFString LIKE and GLOB
+static void Like8(sqlite3_context *context, int argc, sqlite3_value **argv);
+static void Like16(sqlite3_context *context, int argc, sqlite3_value **argv);
+static void Glob8(sqlite3_context *context, int argc, sqlite3_value **argv);
+static void Glob16(sqlite3_context *context, int argc, sqlite3_value **argv);
+
+// The CFLocale of the current user at process start
+static CFLocaleRef gCurrentLocale = NULL;
+
+// Private methods
+@interface GTMSQLiteDatabase (PrivateMethods)
+
+- (int)installCFAdditions;
+- (void)collationArgumentRetain:(NSData *)collationArgs;
+// Convenience method to clean up resources. Called from both
+// dealloc & finalize
+//
+- (void)cleanupDB;
+@end
+
+@implementation GTMSQLiteDatabase
+
++ (void)initialize {
+ // Need the locale for some CFString enhancements
+ gCurrentLocale = CFLocaleCopyCurrent();
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ // Compiling pre-Leopard try to find some symbols dynamically
+ gCFStringCreateWithBytesNoCopySymbol =
+ (CFStringCreateWithBytesNoCopyPtrType)dlsym(
+ RTLD_DEFAULT,
+ kCFStringCreateWithBytesNoCopySymbolName);
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+}
+
++ (int)sqliteVersionNumber {
+ return sqlite3_libversion_number();
+}
+
++ (NSString *)sqliteVersionString {
+ return [NSString stringWithUTF8String:sqlite3_libversion()];
+}
+
+- (id)initWithPath:(NSString *)path
+ withCFAdditions:(BOOL)additions
+ utf8:(BOOL)useUTF8
+ errorCode:(int *)err {
+ int rc = SQLITE_INTERNAL;
+
+ if ((self = [super init])) {
+ path_ = [path copy];
+ if (useUTF8) {
+ rc = sqlite3_open([path_ fileSystemRepresentation], &db_);
+ } else {
+ CFStringEncoding cfEncoding;
+#if __BIG_ENDIAN__
+ cfEncoding = kCFStringEncodingUTF16BE;
+#else
+ cfEncoding = kCFStringEncodingUTF16LE;
+#endif
+ NSStringEncoding nsEncoding
+ = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
+ NSData *data = [path dataUsingEncoding:nsEncoding];
+ // Using -[NSString cStringUsingEncoding] causes sqlite3_open16
+ // to fail because it expects 2 null-terminating bytes and
+ // cStringUsingEncoding only has 1
+ NSMutableData *mutable = [NSMutableData dataWithData:data];
+ [mutable increaseLengthBy:2];
+ rc = sqlite3_open16([mutable bytes], &db_);
+ }
+
+ if ((rc == SQLITE_OK) && db_) {
+ if (additions) {
+ userArgDataPool_ = [[NSMutableArray array] retain];
+ if (!userArgDataPool_) {
+ // Leave *err as internal err
+ // COV_NF_START - not sure how to fail Cocoa initializers
+ [self release];
+ return nil;
+ // COV_NF_END
+ }
+ rc = [self installCFAdditions];
+ }
+ }
+
+ if (err) *err = rc;
+
+ if (rc != SQLITE_OK) {
+ // COV_NF_START
+ [self release];
+ self = nil;
+ // COV_NF_END
+ }
+ }
+
+ return self;
+}
+
+- (id)initInMemoryWithCFAdditions:(BOOL)additions
+ utf8:(BOOL)useUTF8
+ errorCode:(int *)err {
+ return [self initWithPath:@":memory:"
+ withCFAdditions:additions
+ utf8:useUTF8
+ errorCode:err];
+}
+
+#if GTM_SUPPORT_GC
+- (void)finalize {
+ [self cleanupDB];
+ [super finalize];
+}
+#endif
+
+- (void)dealloc {
+ [self cleanupDB];
+ [super dealloc];
+}
+
+- (void)cleanupDB {
+ if (db_) {
+ int rc = sqlite3_close(db_);
+ if (rc != SQLITE_OK) {
+ // COV_NF_START
+ _GTMDevLog(@"Unable to close \"%@\", error code: %d", self, rc);
+ _GTMDevLog(@"Did you forget to call -[GTMSQLiteStatement"
+ @" finalizeStatement] on one of your statements?");
+ // COV_NF_END
+ }
+ }
+ [path_ release];
+ [userArgDataPool_ release];
+}
+
+// Private method to install our custom CoreFoundation additions to SQLite
+// behavior
+- (int)installCFAdditions {
+ int rc = SQLITE_OK;
+ // Install our custom functions for improved text handling
+ // UPPER/LOWER
+ const struct {
+ const char *sqlName;
+ UpperLowerUserArgs userArgs;
+ void *function;
+ } customUpperLower[] = {
+ { "upper", { YES, SQLITE_UTF8 }, &UpperLower8 },
+ { "upper", { YES, SQLITE_UTF16 }, &UpperLower16 },
+ { "upper", { YES, SQLITE_UTF16BE }, &UpperLower16 },
+ { "upper", { YES, SQLITE_UTF16LE }, &UpperLower16 },
+ { "lower", { NO, SQLITE_UTF8 }, &UpperLower8 },
+ { "lower", { NO, SQLITE_UTF16 }, &UpperLower16 },
+ { "lower", { NO, SQLITE_UTF16BE }, &UpperLower16 },
+ { "lower", { NO, SQLITE_UTF16LE }, &UpperLower16 },
+ };
+
+ for (size_t i = 0;
+ i < (sizeof(customUpperLower) / sizeof(customUpperLower[0]));
+ i++) {
+ rc = sqlite3_create_function(db_,
+ customUpperLower[i].sqlName,
+ 1,
+ customUpperLower[i].userArgs.textRep,
+ (void *)&customUpperLower[i].userArgs,
+ customUpperLower[i].function,
+ NULL,
+ NULL);
+ if (rc != SQLITE_OK)
+ return rc; // COV_NF_LINE because sqlite3_create_function is
+ // called with input defined at compile-time
+ }
+
+ // Fixed collation sequences
+ const struct {
+ const char *sqlName;
+ CollateUserArgs userArgs;
+ void *function;
+ } customCollationSequence[] = {
+ { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF8 }, &Collate8 },
+ { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16 }, &Collate16 },
+ { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16BE }, &Collate16 },
+ { "nocase", { NO, kCFCompareCaseInsensitive, SQLITE_UTF16LE }, &Collate16 },
+ };
+
+ for (size_t i = 0;
+ i < (sizeof(customCollationSequence) / sizeof(customCollationSequence[0]));
+ i++) {
+ rc = sqlite3_create_collation(db_,
+ customCollationSequence[i].sqlName,
+ customCollationSequence[i].userArgs.textRep,
+ (void *)&customCollationSequence[i].userArgs,
+ customCollationSequence[i].function);
+ if (rc != SQLITE_OK)
+ return rc; // COV_NF_LINE because the input to
+ // sqlite3_create_collation is set at compile time
+ }
+
+ // Install handler for dynamic collation sequences
+ const struct {
+ const char *sqlName;
+ int numArgs;
+ int textRep;
+ void *function;
+ } customLike[] = {
+ { "like", 2, SQLITE_UTF8, &Like8 },
+ { "like", 2, SQLITE_UTF16, &Like16 },
+ { "like", 2, SQLITE_UTF16BE, &Like16 },
+ { "like", 2, SQLITE_UTF16LE, &Like16 },
+ { "like", 3, SQLITE_UTF8, &Like8 },
+ { "like", 3, SQLITE_UTF16, &Like16 },
+ { "like", 3, SQLITE_UTF16BE, &Like16 },
+ { "like", 3, SQLITE_UTF16LE, &Like16 },
+ };
+
+ rc = sqlite3_collation_needed(db_, self, &CollateNeeded);
+ if (rc != SQLITE_OK)
+ return rc; // COV_NF_LINE because input to
+ // sqlite3_collation_needed is static
+
+ // Start LIKE as case-insensitive and non-literal
+ // (sqlite defaults LIKE to case-insensitive)
+ likeOptions_ = kCFCompareCaseInsensitive | kCFCompareNonliteral;
+ for (size_t i = 0; i < (sizeof(customLike) / sizeof(customLike[0])); i++) {
+ // Each implementation gets its own user args
+ NSMutableData *argsData
+ = [NSMutableData dataWithLength:sizeof(LikeGlobUserArgs)];
+ if (!argsData) return SQLITE_INTERNAL;
+ [userArgDataPool_ addObject:argsData];
+ LikeGlobUserArgs *args = (LikeGlobUserArgs *)[argsData bytes];
+ args->compareOptionPtr = &likeOptions_;
+ args->textRep = customLike[i].textRep;
+ rc = sqlite3_create_function(db_,
+ customLike[i].sqlName,
+ customLike[i].numArgs,
+ customLike[i].textRep,
+ args,
+ customLike[i].function,
+ NULL,
+ NULL);
+ if (rc != SQLITE_OK)
+ return rc; // COV_NF_LINE because input to
+ // sqlite3_create_function is static
+ }
+
+ // Start GLOB just non-literal but case-sensitive (same as SQLite defaults)
+ const struct {
+ const char *sqlName;
+ int textRep;
+ void *function;
+ } customGlob[] = {
+ { "glob", SQLITE_UTF8, &Glob8 },
+ { "glob", SQLITE_UTF16, &Glob16 },
+ { "glob", SQLITE_UTF16BE, &Glob16 },
+ { "glob", SQLITE_UTF16LE, &Glob16 },
+ };
+
+ globOptions_ = kCFCompareNonliteral;
+ for (size_t i = 0; i < (sizeof(customGlob) / sizeof(customGlob[0])); i++) {
+ // Each implementation gets its own user args
+ NSMutableData *argsData
+ = [NSMutableData dataWithLength:sizeof(LikeGlobUserArgs)];
+ if (!argsData) return SQLITE_INTERNAL;
+ [userArgDataPool_ addObject:argsData];
+ LikeGlobUserArgs *args = (LikeGlobUserArgs *)[argsData bytes];
+ args->compareOptionPtr = &globOptions_;
+ args->textRep = customGlob[i].textRep;
+ rc = sqlite3_create_function(db_,
+ customGlob[i].sqlName,
+ 2,
+ customGlob[i].textRep,
+ args,
+ customGlob[i].function,
+ NULL,
+ NULL);
+ if (rc != SQLITE_OK)
+ return rc; // COV_NF_LINE because input to
+ // sqlite3_create_function is static
+ }
+
+ hasCFAdditions_ = YES;
+ return SQLITE_OK;
+}
+
+// Private method used by collation creation callback
+- (void)collationArgumentRetain:(NSData *)collationArgs {
+ [userArgDataPool_ addObject:collationArgs];
+}
+
+- (sqlite3 *)sqlite3DB {
+ return db_;
+}
+
+- (void)synchronousMode:(BOOL)enable {
+ if (enable) {
+ [self executeSQL:@"PRAGMA synchronous = NORMAL;"];
+ [self executeSQL:@"PRAGMA fullfsync = 1;"];
+ } else {
+ [self executeSQL:@"PRAGMA fullfsync = 0;"];
+ [self executeSQL:@"PRAGMA synchronous = OFF;"];
+ }
+}
+
+- (BOOL)hasCFAdditions {
+ return hasCFAdditions_;
+}
+
+- (void)setLikeComparisonOptions:(CFOptionFlags)options {
+ if (hasCFAdditions_)
+ likeOptions_ = FilteredStringCompareFlags(options);
+}
+
+- (CFOptionFlags)likeComparisonOptions {
+ CFOptionFlags flags = 0;
+ if (hasCFAdditions_)
+ flags = likeOptions_;
+ return flags;
+}
+
+- (void)setGlobComparisonOptions:(CFOptionFlags)options {
+ if (hasCFAdditions_)
+ globOptions_ = FilteredStringCompareFlags(options);
+}
+
+- (CFOptionFlags)globComparisonOptions {
+ CFOptionFlags globOptions = 0;
+ if (hasCFAdditions_)
+ globOptions = globOptions_;
+ return globOptions;
+}
+
+- (int)lastErrorCode {
+ return sqlite3_errcode(db_);
+}
+
+- (NSString *)lastErrorString {
+ const char *errMsg = sqlite3_errmsg(db_);
+ if (!errMsg) return nil;
+ return [NSString stringWithCString:errMsg encoding:NSUTF8StringEncoding];
+}
+
+- (int)lastChangeCount {
+ return sqlite3_changes(db_);
+}
+
+- (int)totalChangeCount {
+ return sqlite3_total_changes(db_);
+}
+
+- (unsigned long long)lastInsertRowID {
+ return sqlite3_last_insert_rowid(db_);
+}
+
+- (void)interrupt {
+ sqlite3_interrupt(db_);
+}
+
+- (int)setBusyTimeoutMS:(int)timeoutMS {
+ int rc = sqlite3_busy_timeout(db_, timeoutMS);
+ if (rc == SQLITE_OK) {
+ timeoutMS_ = timeoutMS;
+ }
+ return rc;
+}
+
+- (int)busyTimeoutMS {
+ return timeoutMS_;
+}
+
+- (int)executeSQL:(NSString *)sql {
+ int rc;
+ // Sanity
+ if (!sql) {
+ rc = SQLITE_MISUSE; // Reasonable return for this case
+ } else {
+ if (hasCFAdditions_) {
+ rc = sqlite3_exec(db_,
+ [[sql precomposedStringWithCanonicalMapping]
+ UTF8String],
+ NULL, NULL, NULL);
+ } else {
+ rc = sqlite3_exec(db_, [sql UTF8String], NULL, NULL, NULL);
+ }
+ }
+ return rc;
+}
+
+- (BOOL)beginDeferredTransaction {
+ int err;
+ err = [self executeSQL:@"BEGIN DEFERRED TRANSACTION;"];
+ return (err == SQLITE_OK) ? YES : NO;
+}
+
+- (BOOL)rollback {
+ int err = [self executeSQL:@"ROLLBACK TRANSACTION;"];
+ return (err == SQLITE_OK) ? YES : NO;
+}
+
+- (BOOL)commit {
+ int err = [self executeSQL:@"COMMIT TRANSACTION;"];
+ return (err == SQLITE_OK) ? YES : NO;
+}
+
+- (NSString *)description {
+ return [NSString stringWithFormat:@"<%@: %p - %@>",
+ [self class], self, path_];
+}
+@end
+
+
+#pragma mark Upper/Lower
+
+// Private helper to handle upper/lower conversions for UTF8
+static void UpperLower8(sqlite3_context *context, int argc, sqlite3_value **argv) {
+ // Args
+ if ((argc < 1) || (sqlite3_value_type(argv[0]) == SQLITE_NULL)) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LOWER/UPPER CF implementation got bad args",
+ -1);
+ return;
+ // COV_NF_END
+ }
+ const char *sqlText8 = (void *)sqlite3_value_text(argv[0]);
+ if (!sqlText8) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LOWER/UPPER CF implementation no input UTF8",
+ -1);
+ return;
+ // COV_NF_END
+ }
+
+ // Get user data
+ UpperLowerUserArgs *userArgs = sqlite3_user_data(context);
+ if (!userArgs) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LOWER/UPPER CF implementation no user args",
+ -1);
+ return;
+ // COV_NF_END
+ }
+
+ _GTMDevAssert(userArgs->textRep == SQLITE_UTF8,
+ @"Received non UTF8 encoding in UpperLower8");
+
+ // Worker string, must be mutable for case conversion so order our calls
+ // to only copy once
+ CFMutableStringRef workerString =
+ CFStringCreateMutable(kCFAllocatorDefault, 0);
+ GTMCFAutorelease(workerString);
+ if (!workerString) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation failed " \
+ "to allocate CFMutableStringRef", -1);
+ return;
+ // COV_NF_END
+ }
+ CFStringAppendCString(workerString, sqlText8, kCFStringEncodingUTF8);
+
+ // Perform the upper/lower
+ if (userArgs->upperCase) {
+ CFStringUppercase(workerString, gCurrentLocale);
+ } else {
+ CFStringLowercase(workerString, gCurrentLocale);
+ }
+
+ // Convert to our canonical composition
+ CFStringNormalize(workerString, kCFStringNormalizationFormC);
+
+ // Get the bytes we will return, using the more efficient accessor if we can
+ const char *returnString = CFStringGetCStringPtr(workerString,
+ kCFStringEncodingUTF8);
+ if (returnString) {
+ // COV_NF_START
+ // Direct buffer, but have SQLite copy it
+ sqlite3_result_text(context, returnString, -1, SQLITE_TRANSIENT);
+ // COV_NF_END
+ } else {
+ // Need to get a copy
+ CFIndex workerLength = CFStringGetLength(workerString);
+ CFIndex bufferSize =
+ CFStringGetMaximumSizeForEncoding(workerLength,
+ kCFStringEncodingUTF8);
+ void *returnBuffer = malloc(bufferSize);
+ if (!returnBuffer) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LOWER/UPPER failed to allocate return buffer", -1);
+ return;
+ // COV_NF_END
+ }
+ CFIndex convertedBytes = 0;
+ CFIndex convertedChars = CFStringGetBytes(workerString,
+ CFRangeMake(0, workerLength),
+ kCFStringEncodingUTF8,
+ 0,
+ false,
+ returnBuffer,
+ bufferSize,
+ &convertedBytes);
+ if (convertedChars != workerLength) {
+ // COV_NF_START
+ free(returnBuffer);
+ sqlite3_result_error(context,
+ "CFStringGetBytes() failed to " \
+ "convert all characters", -1);
+ // COV_NF_END
+ } else {
+ // Set the result, letting SQLite take ownership and using free() as
+ // the destructor
+ // We cast the 3rd parameter to an int because sqlite3 doesn't appear
+ // to support 64-bit mode.
+ sqlite3_result_text(context, returnBuffer, (int)convertedBytes, &free);
+ }
+ }
+}
+
+// Private helper to handle upper/lower conversions for UTF16 variants
+static void UpperLower16(sqlite3_context *context,
+ int argc, sqlite3_value **argv) {
+ // Args
+ if ((argc < 1) || (sqlite3_value_type(argv[0]) == SQLITE_NULL)) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LOWER/UPPER CF implementation got bad args", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // For UTF16 variants we want our working string to be in native-endian
+ // UTF16. This gives us the fewest number of copies (since SQLite converts
+ // in-place). There is no advantage to breaking out the string construction
+ // to use UTF16BE or UTF16LE because all that does is move the conversion
+ // work into the CFString constructor, so just use simple code.
+ int sqlText16ByteCount = sqlite3_value_bytes16(argv[0]);
+ const UniChar *sqlText16 = (void *)sqlite3_value_text16(argv[0]);
+ if (!sqlText16ByteCount || !sqlText16) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation no input UTF16", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // Get user data
+ UpperLowerUserArgs *userArgs = sqlite3_user_data(context);
+ if (!userArgs) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LOWER/UPPER CF implementation no user args", -1);
+ return;
+ // COV_NF_END
+ }
+ CFStringEncoding encoding = SqliteTextEncodingToCFStringEncoding(userArgs->textRep);
+
+ // Mutable worker for upper/lower
+ CFMutableStringRef workerString = CFStringCreateMutable(kCFAllocatorDefault, 0);
+ GTMCFAutorelease(workerString);
+ if (!workerString) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation failed " \
+ "to allocate CFMutableStringRef", -1);
+ return;
+ // COV_NF_END
+ }
+ CFStringAppendCharacters(workerString, sqlText16,
+ sqlText16ByteCount / sizeof(UniChar));
+ // Perform the upper/lower
+ if (userArgs->upperCase) {
+ CFStringUppercase(workerString, gCurrentLocale);
+ } else {
+ CFStringLowercase(workerString, gCurrentLocale);
+ }
+ // Convert to our canonical composition
+ CFStringNormalize(workerString, kCFStringNormalizationFormC);
+
+ // Length after normalization matters
+ CFIndex workerLength = CFStringGetLength(workerString);
+
+ // If we can give direct byte access use it
+ const UniChar *returnString = CFStringGetCharactersPtr(workerString);
+ if (returnString) {
+ // COV_NF_START details of whether cfstringgetcharactersptr returns
+ // a buffer or NULL are internal; not something we can depend on.
+ // When building for Leopard+, CFIndex is a 64-bit type, which is
+ // why we cast it to an int when we call the sqlite api.
+ _GTMDevAssert((workerLength * sizeof(UniChar) <= INT_MAX),
+ @"sqlite methods do not support buffers greater "
+ @"than 32 bit sizes");
+ // Direct access to the internal buffer, hand it to sqlite for copy and
+ // conversion
+ sqlite3_result_text16(context, returnString,
+ (int)(workerLength * sizeof(UniChar)),
+ SQLITE_TRANSIENT);
+ // COV_NF_END
+ } else {
+ // Need to get a copy since we can't get direct access
+ CFIndex bufferSize = CFStringGetMaximumSizeForEncoding(workerLength,
+ encoding);
+ void *returnBuffer = malloc(bufferSize);
+ if (!returnBuffer) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation failed " \
+ "to allocate return buffer", -1);
+ return;
+ // COV_NF_END
+ }
+ CFIndex convertedBytes = 0;
+ CFIndex convertedChars = CFStringGetBytes(workerString,
+ CFRangeMake(0, workerLength),
+ encoding,
+ 0,
+ false,
+ returnBuffer,
+ bufferSize,
+ &convertedBytes);
+ if (convertedChars != workerLength) {
+ // COV_NF_START
+ free(returnBuffer);
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation CFStringGetBytes() " \
+ "failed to convert all characters", -1);
+ // COV_NF_END
+ } else {
+ // When building for Leopard+, CFIndex is a 64-bit type, but
+ // sqlite3's functions all take ints. Assert the error for dev
+ // builds and cast down.
+ _GTMDevAssert((convertedBytes <= INT_MAX),
+ @"sqlite methods do not support buffers greater "
+ @"than 32-bit sizes");
+ int convertedBytesForSQLite = (int)convertedBytes;
+ // Set the result, letting SQLite take ownership and using free() as
+ // the destructor. For output since we're copying out the bytes anyway
+ // we might as well use the preferred encoding of the original call.
+ _GTMDevAssert(userArgs->textRep == SQLITE_UTF16BE ||
+ userArgs->textRep == SQLITE_UTF16LE,
+ @"Received non UTF8 encoding in UpperLower8");
+ switch (userArgs->textRep) {
+ case SQLITE_UTF16BE:
+ sqlite3_result_text16be(context, returnBuffer,
+ convertedBytesForSQLite, &free);
+ break;
+ case SQLITE_UTF16LE:
+ sqlite3_result_text16le(context, returnBuffer,
+ convertedBytesForSQLite, &free);
+ break;
+ default:
+ // COV_NF_START no way to tell sqlite to not use utf8 or utf16?
+ sqlite3_result_error(context,
+ "LOWER/UPPER CF implementation " \
+ "had unhandled encoding", -1);
+ // COV_NF_END
+ }
+ }
+ }
+}
+
+
+#pragma mark Collations
+
+static void CollateNeeded(void *userContext, sqlite3 *db, int textRep,
+ const char *name) {
+ // Cast
+ GTMSQLiteDatabase *gtmdb = (GTMSQLiteDatabase *)userContext;
+ _GTMDevAssert(gtmdb, @"Invalid database parameter from sqlite");
+
+ // Create space for the collation args
+ NSMutableData *collationArgsData =
+ [NSMutableData dataWithLength:sizeof(CollateUserArgs)];
+ CollateUserArgs *userArgs = (CollateUserArgs *)[collationArgsData bytes];
+ bzero(userArgs, sizeof(CollateUserArgs));
+ userArgs->textRep = textRep;
+
+ // Parse the name into the flags we need
+ NSString *collationName =
+ [[NSString stringWithUTF8String:name] lowercaseString];
+ NSArray *collationComponents =
+ [collationName componentsSeparatedByString:@"_"];
+ NSString *collationFlag = nil;
+ BOOL atLeastOneValidFlag = NO;
+ GTM_FOREACH_OBJECT(collationFlag, collationComponents) {
+ if ([collationFlag isEqualToString:@"reverse"]) {
+ userArgs->reverse = YES;
+ atLeastOneValidFlag = YES;
+ } else if ([collationFlag isEqualToString:@"nocase"]) {
+ userArgs->compareOptions |= kCFCompareCaseInsensitive;
+ atLeastOneValidFlag = YES;
+ } else if ([collationFlag isEqualToString:@"nonliteral"]) {
+ userArgs->compareOptions |= kCFCompareNonliteral;
+ atLeastOneValidFlag = YES;
+ } else if ([collationFlag isEqualToString:@"localized"]) {
+ userArgs->compareOptions |= kCFCompareLocalized;
+ atLeastOneValidFlag = YES;
+ } else if ([collationFlag isEqualToString:@"numeric"]) {
+ userArgs->compareOptions |= kCFCompareNumerically;
+ atLeastOneValidFlag = YES;
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+ } else if ([collationFlag isEqualToString:@"nodiacritic"]) {
+ userArgs->compareOptions |= kCFCompareDiacriticInsensitive;
+ atLeastOneValidFlag = YES;
+ } else if ([collationFlag isEqualToString:@"widthinsensitive"]) {
+ userArgs->compareOptions |= kCFCompareWidthInsensitive;
+ atLeastOneValidFlag = YES;
+#else
+ } else if (([collationFlag isEqualToString:@"nodiacritic"]) ||
+ ([collationFlag isEqualToString:@"widthinsensitive"])) {
+ _GTMDevLog(@"GTMSQLiteDatabase 10.5 collating not "
+ @"available on 10.4 or earlier");
+#endif
+ }
+ }
+
+ // No valid tokens means nothing to do
+ if (!atLeastOneValidFlag) return;
+
+ int err;
+ // Add the collation
+ switch (textRep) {
+ case SQLITE_UTF8:
+ err = sqlite3_create_collation([gtmdb sqlite3DB], name,
+ textRep, userArgs, &Collate8);
+ if (err != SQLITE_OK) return;
+ break;
+ case SQLITE_UTF16:
+ case SQLITE_UTF16BE:
+ case SQLITE_UTF16LE:
+ err = sqlite3_create_collation([gtmdb sqlite3DB], name,
+ textRep, userArgs, &Collate16);
+ if (err != SQLITE_OK) return;
+ break;
+ default:
+ return;
+ }
+
+ // Have the db retain our collate function args
+ [gtmdb collationArgumentRetain:collationArgsData];
+}
+
+static int Collate8(void *userContext, int length1, const void *str1,
+ int length2, const void *str2) {
+ // User args
+ CollateUserArgs *userArgs = (CollateUserArgs *)userContext;
+ _GTMDevAssert(userArgs, @"Invalid user arguments from sqlite");
+
+ // Sanity and zero-lengths
+ if (!(str1 && str2) || (!length1 && !length2)) {
+ return kCFCompareEqualTo; // Best we can do and stable sort
+ }
+ if (!length1 && length2) {
+ if (userArgs->reverse) {
+ return kCFCompareGreaterThan;
+ } else {
+ return kCFCompareLessThan;
+ }
+ } else if (length1 && !length2) {
+ if (userArgs->reverse) {
+ return kCFCompareLessThan;
+ } else {
+ return kCFCompareGreaterThan;
+ }
+ }
+
+ // We have UTF8 strings with no terminating null, we want to compare
+ // with as few copies as possible. Leopard introduced a no-copy string
+ // creation function, we'll use it when we can but we want to stay compatible
+ // with Tiger.
+ CFStringRef string1 = NULL, string2 = NULL;
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ if (gCFStringCreateWithBytesNoCopySymbol) {
+ string1 = gCFStringCreateWithBytesNoCopySymbol(kCFAllocatorDefault,
+ str1,
+ length1,
+ kCFStringEncodingUTF8,
+ false,
+ kCFAllocatorNull);
+ string2 = gCFStringCreateWithBytesNoCopySymbol(kCFAllocatorDefault,
+ str2,
+ length2,
+ kCFStringEncodingUTF8,
+ false,
+ kCFAllocatorNull);
+ } else {
+ // Have to use the copy-based variants
+ string1 = CFStringCreateWithBytes(kCFAllocatorDefault,
+ str1,
+ length1,
+ kCFStringEncodingUTF8,
+ false);
+ string2 = CFStringCreateWithBytes(kCFAllocatorDefault,
+ str2,
+ length2,
+ kCFStringEncodingUTF8,
+ false);
+ }
+#else // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ string1 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault,
+ str1,
+ length1,
+ kCFStringEncodingUTF8,
+ false,
+ kCFAllocatorNull);
+ string2 = CFStringCreateWithBytesNoCopy(kCFAllocatorDefault,
+ str2,
+ length2,
+ kCFStringEncodingUTF8,
+ false,
+ kCFAllocatorNull);
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ GTMCFAutorelease(string1);
+ GTMCFAutorelease(string2);
+ // Allocation failures can't really be sanely handled from a collator
+ int sqliteResult;
+ if (!(string1 && string2)) {
+ // COV_NF_START
+ sqliteResult = (int)kCFCompareEqualTo;
+ // COV_NF_END
+ } else {
+ // Compare
+ // We have to cast to int because SQLite takes functions that
+ // return an int, but when compiling for Leopard+,
+ // CFComparisonResult is a signed long, but on Tiger it's an int
+ CFComparisonResult result;
+ result = CFStringCompare(string1,
+ string2,
+ userArgs->compareOptions);
+ sqliteResult = (int)result;
+ // Reverse
+ if (userArgs->reverse && sqliteResult) {
+ sqliteResult = -sqliteResult;
+ }
+
+ }
+ return sqliteResult;
+}
+
+static int Collate16(void *userContext, int length1, const void *str1,
+ int length2, const void *str2) {
+ // User args
+ CollateUserArgs *userArgs = (CollateUserArgs *)userContext;
+ _GTMDevAssert(userArgs, @"Invalid user arguments from sqlite");
+
+ // Sanity and zero-lengths
+ if (!(str1 && str2) || (!length1 && !length2)) {
+ return kCFCompareEqualTo; // Best we can do and stable sort
+ }
+ if (!length1 && length2) {
+ if (userArgs->reverse) {
+ return kCFCompareGreaterThan;
+ } else {
+ return kCFCompareLessThan;
+ }
+ } else if (length1 && !length2) {
+ if (userArgs->reverse) {
+ return kCFCompareLessThan;
+ } else {
+ return kCFCompareGreaterThan;
+ }
+ }
+
+ // Target encoding
+ CFStringEncoding encoding =
+ SqliteTextEncodingToCFStringEncoding(userArgs->textRep);
+
+ // We have UTF16 strings, we want to compare with as few copies as
+ // possible. Since endianness matters we want to use no-copy
+ // variants where possible and copy (and endian convert) only when
+ // we must.
+ CFStringRef string1 = NULL, string2 = NULL;
+ if ((userArgs->textRep == SQLITE_UTF16) ||
+#if defined __BIG_ENDIAN__
+ (userArgs->textRep == SQLITE_UTF16BE)
+#elif defined __LITTLE_ENDIAN__
+ (userArgs->textRep == SQLITE_UTF16LE)
+#else
+#error Not big or little endian?
+#endif
+ ) {
+ string1 = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ str1,
+ length1 / sizeof(UniChar),
+ kCFAllocatorNull);
+ string2 = CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ str2,
+ length2 / sizeof(UniChar),
+ kCFAllocatorNull);
+ } else {
+ // No point in using the "no copy" version of the call here. If the
+ // bytes were in the native order we'd be in the other part of this
+ // conditional, so we know we have to copy the string to endian convert
+ // it.
+ string1 = CFStringCreateWithBytes(kCFAllocatorDefault,
+ str1,
+ length1,
+ encoding,
+ false);
+ string2 = CFStringCreateWithBytes(kCFAllocatorDefault,
+ str2,
+ length2,
+ encoding,
+ false);
+ }
+
+ GTMCFAutorelease(string1);
+ GTMCFAutorelease(string2);
+ int sqliteResult;
+ // Allocation failures can't really be sanely handled from a collator
+ if (!(string1 && string2)) {
+ // COV_NF_START
+ sqliteResult = (int)kCFCompareEqualTo;
+ // COV_NF_END
+ } else {
+ // Compare
+ // We cast the return value to an int because CFComparisonResult
+ // is a long in Leopard+ builds. I have no idea why we need
+ // 64-bits for a 3-value enum, but that's how it is...
+ CFComparisonResult result;
+ result = CFStringCompare(string1,
+ string2,
+ userArgs->compareOptions);
+
+ sqliteResult = (int)result;
+ //Reverse
+ if (userArgs->reverse && sqliteResult) {
+ sqliteResult = -sqliteResult;
+ }
+ }
+
+ return sqliteResult;
+}
+
+
+#pragma mark Like/Glob
+
+// Private helper to handle LIKE and GLOB with different encodings. This
+// is essentially a reimplementation of patternCompare() in func.c of the
+// SQLite sources.
+static void LikeGlobCompare(sqlite3_context *context,
+ CFStringRef pattern,
+ CFStringRef targetString,
+ UniChar matchAll,
+ UniChar matchOne,
+ UniChar escape,
+ BOOL setSupport,
+ CFOptionFlags compareOptions) {
+ // Setup for pattern walk
+ CFIndex patternLength = CFStringGetLength(pattern);
+ CFStringInlineBuffer patternBuffer;
+ CFStringInitInlineBuffer(pattern,
+ &patternBuffer,
+ CFRangeMake(0, patternLength));
+ UniChar patternChar;
+ CFIndex patternIndex = 0;
+ CFIndex targetStringLength = CFStringGetLength(targetString);
+ CFIndex targetStringIndex = 0;
+ BOOL isAnchored = YES;
+
+ size_t dataSize = patternLength * sizeof(UniChar);
+ NSMutableData *tempData = [NSMutableData dataWithLength:dataSize];
+ // Temp string buffer can be no larger than the whole pattern
+ UniChar *findBuffer = [tempData mutableBytes];
+ if (!findBuffer) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation failed to " \
+ "allocate temporary buffer", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // We'll use a mutable string we can just reset as we wish
+ CFMutableStringRef findString =
+ CFStringCreateMutableWithExternalCharactersNoCopy(kCFAllocatorDefault,
+ NULL,
+ 0,
+ 0,
+ kCFAllocatorNull);
+ GTMCFAutorelease(findString);
+ if (!findString) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation failed to " \
+ "allocate temporary CFString", -1);
+ return;
+ // COV_NF_END
+ }
+ // Walk the pattern
+ while (patternIndex < patternLength) {
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ // Match all character has no effect other than to unanchor the search
+ if (patternChar == matchAll) {
+ isAnchored = NO;
+ patternIndex++;
+ continue;
+ }
+ // Match one character pushes the string index forward by one composed
+ // character
+ if (patternChar == matchOne) {
+ // If this single char match would walk us off the end of the string
+ // we're already done, no match
+ if (targetStringIndex >= targetStringLength) {
+ sqlite3_result_int(context, 0);
+ return;
+ }
+ // There's still room in the string, so move the string index forward one
+ // composed character and go back around.
+ CFRange nextCharRange =
+ CFStringGetRangeOfComposedCharactersAtIndex(targetString,
+ targetStringIndex);
+ targetStringIndex = nextCharRange.location + nextCharRange.length;
+ patternIndex++;
+ continue;
+ }
+ // Character set matches require the parsing of the character set
+ if (setSupport && (patternChar == 0x5B)) { // "["
+ // A character set must match one character, if there's not at least one
+ // character left in the string, don't bother
+ if (targetStringIndex >= targetStringLength) {
+ sqlite3_result_int(context, 0);
+ return;
+ }
+ // There's at least one character, try to match the remainder of the
+ // string using a CFCharacterSet
+ CFMutableCharacterSetRef charSet
+ = CFCharacterSetCreateMutable(kCFAllocatorDefault);
+ GTMCFAutorelease(charSet);
+ if (!charSet) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation failed to " \
+ "allocate temporary CFMutableCharacterSet", -1);
+ return;
+ // COV_NF_END
+ }
+
+ BOOL invert = NO;
+ // Walk one character forward
+ patternIndex++;
+ if (patternIndex >= patternLength) {
+ // Oops, out of room
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation found " \
+ "unclosed character set", -1);
+ return;
+ }
+ // First character after pattern open is special-case
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ if (patternChar == 0x5E) { // "^"
+ invert = YES;
+ // Bump forward one character, can still be an unescaped "]" after
+ // negation
+ patternIndex++;
+ if (patternIndex >= patternLength) {
+ // Oops, out of room
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation found " \
+ "unclosed character set after negation", -1);
+ return;
+ }
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ }
+ // First char in set or first char in negation can be a literal "]" not
+ // considered a close
+ if (patternChar == 0x5D) { // "]"
+ CFCharacterSetAddCharactersInRange(charSet,
+ CFRangeMake(patternChar, 1));
+ patternIndex++;
+ if (patternIndex >= patternLength) {
+ // Oops, out of room
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation found " \
+ "unclosed character set after escaped ]", -1);
+ return;
+ }
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ }
+ while ((patternIndex < patternLength) &&
+ patternChar &&
+ (patternChar != 0x5D)) { // "]"
+ // Check for possible character range, for this to be true we
+ // must have a hyphen at the next position and at least 3
+ // characters of room (for hyphen, range end, and set
+ // close). Hyphens at the end without a trailing range are
+ // treated as literals
+ if (((patternLength - patternIndex) >= 3) &&
+ // Second char must be "-"
+ (CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ // 0x2D is "-"
+ patternIndex + 1) == 0x2D) &&
+ // And third char must be anything other than set close in
+ // case the hyphen is at the end of the set and needs to
+ // be treated as a literal
+ (CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex + 2)
+ != 0x5D)) { // "]"
+ // Get the range close
+ UniChar rangeClose =
+ CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex + 2);
+ // Add the whole range
+ int rangeLen = rangeClose - patternChar + 1;
+ CFCharacterSetAddCharactersInRange(charSet,
+ CFRangeMake(patternChar,
+ rangeLen));
+ // Move past the end of the range
+ patternIndex += 3;
+ } else {
+ // Single Raw character
+ CFCharacterSetAddCharactersInRange(charSet,
+ CFRangeMake(patternChar, 1));
+ patternIndex++;
+ }
+ // Load next char for loop
+ if (patternIndex < patternLength) {
+ patternChar =
+ CFStringGetCharacterFromInlineBuffer(&patternBuffer, patternIndex);
+ } else {
+ patternChar = 0;
+ }
+ }
+ // Check for closure
+ if (patternChar != 0x5D) { // "]"
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation found " \
+ "unclosed character set", -1);
+ return;
+ } else {
+ // Increment past the end of the set
+ patternIndex++;
+ }
+ // Invert the set if needed
+ if (invert) CFCharacterSetInvert(charSet);
+ // Do the search
+ CFOptionFlags findOptions = 0;
+ if (isAnchored) findOptions |= kCFCompareAnchored;
+ CFRange foundRange;
+ unsigned long rangeLen = targetStringLength - targetStringIndex;
+ BOOL found = CFStringFindCharacterFromSet(targetString,
+ charSet,
+ CFRangeMake(targetStringIndex,
+ rangeLen),
+ findOptions,
+ &foundRange);
+ // If no match then the whole pattern fails
+ if (!found) {
+ sqlite3_result_int(context, 0);
+ return;
+ }
+ // If we did match then we need to push the string index to the
+ // character past the end of the match and then go back around
+ // the loop.
+ targetStringIndex = foundRange.location + foundRange.length;
+ // At this point patternIndex is either at the end of the
+ // string, or at the next special character which will be picked
+ // up and handled at the top of the loop. We do, however, need
+ // to reset the anchor status
+ isAnchored = YES;
+ // End of character sets, back around
+ continue;
+ }
+ // Otherwise the pattern character is a normal or escaped
+ // character we should consume and match with normal string
+ // matching
+ CFIndex findBufferIndex = 0;
+ while ((patternIndex < patternLength) && patternChar &&
+ !((patternChar == matchAll) || (patternChar == matchOne) ||
+ (setSupport && (patternChar == 0x5B)))) { // "["
+ if (patternChar == escape) {
+ // COV_NF_START - this code doesn't appear to be hittable
+ // setSupport implies that the escape char is NULL
+ // additionally, setSupport implies GLOB, and running a GLOB
+ // query with an escape clause appears to not work in SQLITE3
+ // when I tried it from the command line
+ // No matter what the character follows the escape copy it to the
+ // buffer
+ patternIndex++;
+ if (patternIndex >= patternLength) {
+ // Oops, escape came at end of pattern, that's an error
+ sqlite3_result_error(context,
+ "LIKE or GLOB CF implementation found " \
+ "escape character at end of pattern", -1);
+ return;
+ }
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ // COV_NF_END
+ }
+ // At this point the patternChar is either the escaped character or the
+ // original normal character
+ findBuffer[findBufferIndex++] = patternChar;
+ // Set up for next loop
+ patternIndex++;
+ if (patternIndex < patternLength) {
+ patternChar = CFStringGetCharacterFromInlineBuffer(&patternBuffer,
+ patternIndex);
+ } else {
+ patternChar = 0;
+ }
+ }
+ // On loop exit we have a string ready for comparision, if that
+ // string is too long then it can't be a match.
+ if (findBufferIndex > (targetStringLength - targetStringIndex)) {
+ sqlite3_result_int(context, 0);
+ return;
+ }
+
+ // We actually need to do a comparison
+ CFOptionFlags findOptions = compareOptions;
+ if (isAnchored) findOptions |= kCFCompareAnchored;
+ CFStringSetExternalCharactersNoCopy(findString,
+ findBuffer,
+ findBufferIndex,
+ findBufferIndex);
+ CFRange foundRange;
+ unsigned long rangeLen = targetStringLength - targetStringIndex;
+ BOOL found = CFStringFindWithOptions(targetString,
+ findString,
+ CFRangeMake(targetStringIndex,
+ rangeLen),
+ findOptions,
+ &foundRange);
+ // If no match then the whole pattern fails
+ if (!found) {
+ sqlite3_result_int(context, 0);
+ return;
+ }
+ // If we did match then we need to push the string index to the
+ // character past the end of the match and then go back around the
+ // loop.
+ targetStringIndex = foundRange.location + foundRange.length;
+ // At this point patternIndex is either at the end of the string,
+ // or at the next special character which will be picked up and
+ // handled at the top of the loop. We do, however, need to reset
+ // the anchor status
+ isAnchored = YES;
+ }
+ // On loop exit all pattern characters have been considered. If we're still
+ // alive it means that we've matched the entire pattern, except for trailing
+ // wildcards, we need to handle that case.
+ if (isAnchored) {
+ // If we're still anchored there was no trailing matchAll, in which case
+ // we have to have run to exactly the end of the string
+ if (targetStringIndex == targetStringLength) {
+ sqlite3_result_int(context, 1);
+ } else {
+ sqlite3_result_int(context, 0);
+ }
+ } else {
+ // If we're not anchored any remaining characters are OK
+ sqlite3_result_int(context, 1);
+ }
+}
+
+static void Like8(sqlite3_context *context, int argc, sqlite3_value **argv) {
+ // Get our LIKE options
+ LikeGlobUserArgs *likeArgs = sqlite3_user_data(context);
+ if (!likeArgs) {
+ // COV_NF_START
+ sqlite3_result_error(context, "LIKE CF implementation no user args", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // Read the strings
+ const char *pattern = (const char *)sqlite3_value_text(argv[0]);
+ const char *target = (const char *)sqlite3_value_text(argv[1]);
+ if (!pattern || !target) {
+ sqlite3_result_error(context,
+ "LIKE CF implementation missing pattern or value", -1);
+ return;
+ }
+ CFStringRef patternString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ pattern,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ CFStringRef targetString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ target,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ GTMCFAutorelease(patternString);
+ GTMCFAutorelease(targetString);
+ if (!(patternString && targetString)) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE CF implementation failed " \
+ "to allocate CFStrings", -1);
+ return;
+ // COV_NF_END
+ }
+
+ UniChar escapeChar = 0;
+ // If there is a third argument it is the escape character
+ if (argc == 3) {
+ const char *escape = (const char *)sqlite3_value_text(argv[2]);
+ if (!escape) {
+ sqlite3_result_error(context,
+ "LIKE CF implementation missing " \
+ "escape character", -1);
+ return;
+ }
+ CFStringRef escapeString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ escape,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ GTMCFAutorelease(escapeString);
+ if (!escapeString) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE CF implementation failed to " \
+ "allocate CFString for ESCAPE", -1);
+ return;
+ // COV_NF_END
+ }
+ if (CFStringGetLength(escapeString) != 1) {
+ sqlite3_result_error(context,
+ "CF implementation ESCAPE expression " \
+ "must be single character", -1);
+ return;
+ }
+ escapeChar = CFStringGetCharacterAtIndex(escapeString, 0);
+ }
+
+ // Do the compare
+ LikeGlobCompare(context,
+ patternString,
+ targetString,
+ 0x25, // %
+ 0x5F, // _
+ escapeChar,
+ NO, // LIKE does not support character sets
+ *(likeArgs->compareOptionPtr));
+}
+
+static void Like16(sqlite3_context *context, int argc, sqlite3_value **argv) {
+ // Get our LIKE options
+ LikeGlobUserArgs *likeArgs = sqlite3_user_data(context);
+ if (!likeArgs) {
+ // COV_NF_START - sql parser chokes if we feed any input
+ // that could trigger this
+ sqlite3_result_error(context, "LIKE CF implementation no user args", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // For UTF16 variants we want our working string to be in native-endian
+ // UTF16. This gives us the fewest number of copies (since SQLite converts
+ // in-place). There is no advantage to breaking out the string construction
+ // to use UTF16BE or UTF16LE because all that does is move the conversion
+ // work into the CFString constructor, so just use simple code.
+ int patternByteCount = sqlite3_value_bytes16(argv[0]);
+ const UniChar *patternText = (void *)sqlite3_value_text16(argv[0]);
+ int targetByteCount = sqlite3_value_bytes16(argv[1]);
+ const UniChar *targetText = (void *)sqlite3_value_text16(argv[1]);
+ if (!patternByteCount || !patternText || !targetByteCount || !targetText) {
+ sqlite3_result_error(context,
+ "LIKE CF implementation missing pattern or value", -1);
+ return;
+ }
+ CFStringRef patternString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ patternText,
+ patternByteCount / sizeof(UniChar),
+ kCFAllocatorNull);
+ CFStringRef targetString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ targetText,
+ targetByteCount / sizeof(UniChar),
+ kCFAllocatorNull);
+ GTMCFAutorelease(patternString);
+ GTMCFAutorelease(targetString);
+ if (!(patternString && targetString)) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE CF implementation failed " \
+ "to allocate CFStrings", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // If there is a third argument it is the escape character, force a
+ // UTF8 conversion for simplicity
+ UniChar escapeChar = 0;
+ if (argc == 3) {
+ const char *escape = (const char *)sqlite3_value_text(argv[2]);
+ if (!escape) {
+ sqlite3_result_error(context,
+ "LIKE CF implementation " \
+ "missing escape character", -1);
+ return;
+ }
+ CFStringRef escapeString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ escape,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ GTMCFAutorelease(escapeString);
+ if (!escapeString) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "LIKE CF implementation failed to " \
+ "allocate CFString for ESCAPE", -1);
+ return;
+ // COV_NF_END
+ }
+ if (CFStringGetLength(escapeString) != 1) {
+ sqlite3_result_error(context,
+ "CF implementation ESCAPE expression " \
+ "must be single character", -1);
+ return;
+ }
+ escapeChar = CFStringGetCharacterAtIndex(escapeString, 0);
+ }
+
+ // Do the compare
+ LikeGlobCompare(context,
+ patternString,
+ targetString,
+ 0x25, // %
+ 0x5F, // _
+ escapeChar,
+ NO, // LIKE does not support character sets
+ *(likeArgs->compareOptionPtr));
+}
+
+static void Glob8(sqlite3_context *context, int argc, sqlite3_value **argv) {
+ // Get our GLOB options
+ LikeGlobUserArgs *globArgs = sqlite3_user_data(context);
+ if (!globArgs) {
+ // COV_NF_START
+ sqlite3_result_error(context, "GLOB CF implementation no user args", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // Read the strings
+ const char *pattern = (const char *)sqlite3_value_text(argv[0]);
+ const char *target = (const char *)sqlite3_value_text(argv[1]);
+ if (!pattern || !target) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "GLOB CF implementation missing " \
+ "pattern or value", -1);
+ return;
+ // COV_NF_END
+ }
+ CFStringRef patternString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ pattern,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ CFStringRef targetString =
+ CFStringCreateWithCStringNoCopy(kCFAllocatorDefault,
+ target,
+ kCFStringEncodingUTF8,
+ kCFAllocatorNull);
+ GTMCFAutorelease(patternString);
+ GTMCFAutorelease(targetString);
+
+ if (!(patternString && targetString)) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "GLOB CF implementation failed to " \
+ "allocate CFStrings", -1);
+ // COV_NF_END
+ } else {
+ // Do the compare
+ LikeGlobCompare(context,
+ patternString,
+ targetString,
+ 0x2A, // *
+ 0x3F, // ?
+ 0, // GLOB does not support escape characters
+ YES, // GLOB supports character sets
+ *(globArgs->compareOptionPtr));
+ }
+}
+
+static void Glob16(sqlite3_context *context, int argc, sqlite3_value **argv) {
+ // Get our GLOB options
+ LikeGlobUserArgs *globArgs = sqlite3_user_data(context);
+ if (!globArgs) {
+ // COV_NF_START
+ sqlite3_result_error(context, "GLOB CF implementation no user args", -1);
+ return;
+ // COV_NF_END
+ }
+
+ // For UTF16 variants we want our working string to be in
+ // native-endian UTF16. This gives us the fewest number of copies
+ // (since SQLite converts in-place). There is no advantage to
+ // breaking out the string construction to use UTF16BE or UTF16LE
+ // because all that does is move the conversion work into the
+ // CFString constructor, so just use simple code.
+ int patternByteCount = sqlite3_value_bytes16(argv[0]);
+ const UniChar *patternText = (void *)sqlite3_value_text16(argv[0]);
+ int targetByteCount = sqlite3_value_bytes16(argv[1]);
+ const UniChar *targetText = (void *)sqlite3_value_text16(argv[1]);
+ if (!patternByteCount || !patternText || !targetByteCount || !targetText) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "GLOB CF implementation missing pattern or value", -1);
+ return;
+ // COV_NF_END
+ }
+ CFStringRef patternString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ patternText,
+ patternByteCount / sizeof(UniChar),
+ kCFAllocatorNull);
+ CFStringRef targetString =
+ CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault,
+ targetText,
+ targetByteCount / sizeof(UniChar),
+ kCFAllocatorNull);
+ GTMCFAutorelease(patternString);
+ GTMCFAutorelease(targetString);
+ if (!(patternString && targetString)) {
+ // COV_NF_START
+ sqlite3_result_error(context,
+ "GLOB CF implementation failed to "\
+ "allocate CFStrings", -1);
+ // COV_NF_END
+ } else {
+ // Do the compare
+ LikeGlobCompare(context,
+ patternString,
+ targetString,
+ 0x2A, // *
+ 0x3F, // ?
+ 0, // GLOB does not support escape characters
+ YES, // GLOB supports character sets
+ *(globArgs->compareOptionPtr));
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@implementation GTMSQLiteStatement
+
+#pragma mark Creation, Access and Finalization
+
++ (id)statementWithSQL:(NSString *)sql
+ inDatabase:(GTMSQLiteDatabase *)gtmdb
+ errorCode:(int *)err {
+ return [[[GTMSQLiteStatement alloc] initWithSQL:sql
+ inDatabase:gtmdb
+ errorCode:err]
+ autorelease];
+}
+
+- (id)initWithSQL:(NSString *)sql
+ inDatabase:(GTMSQLiteDatabase *)gtmdb
+ errorCode:(int *)err {
+ int rc;
+ id obj;
+ if ((self = [super init])) {
+ // Sanity
+ obj = self;
+ if (sql && gtmdb) {
+ // Find out if the database is using our CF extensions
+ hasCFAdditions_ = [gtmdb hasCFAdditions];
+
+ // Prepare
+ if (hasCFAdditions_) {
+ sql = [sql precomposedStringWithCanonicalMapping];
+ }
+ if (sql) {
+ rc = sqlite3_prepare([gtmdb sqlite3DB],
+ [sql UTF8String],
+ -1,
+ &statement_,
+ NULL);
+ if (rc != SQLITE_OK) {
+ [self release];
+ obj = nil;
+ }
+ } else {
+ // COV_NF_START
+ rc = SQLITE_INTERNAL;
+ [self release];
+ obj = nil;
+ // COV_NF_END
+ }
+ } else {
+ rc = SQLITE_MISUSE;
+ [self release];
+ obj = nil;
+ }
+ } else {
+ // COV_NF_START
+ rc = SQLITE_INTERNAL;
+ obj = nil;
+ // COV_NF_END
+ }
+ if (err) *err = rc;
+ return obj;
+}
+
+#if GTM_SUPPORT_GC
+- (void)finalize {
+ _GTMDevAssert(!statement_,
+ @"-[GTMSQLiteStatement finalizeStatement] must be called when"
+ @" statement is no longer needed");
+ [super finalize];
+}
+#endif
+
+- (void)dealloc {
+ _GTMDevAssert(!statement_,
+ @"-[GTMSQLiteStatement finalizeStatement] must be called when"
+ @" statement is no longer needed");
+ [super dealloc];
+}
+
+- (sqlite3_stmt *)sqlite3Statement {
+ return statement_;
+}
+
+- (int)finalizeStatement {
+ if (!statement_) return SQLITE_MISUSE;
+ int rc = sqlite3_finalize(statement_);
+ statement_ = NULL;
+ return rc;
+}
+
+#pragma mark Parameters and Binding
+
+- (int)parameterCount {
+ if (!statement_) return -1;
+ return sqlite3_bind_parameter_count(statement_);
+}
+
+- (int)positionOfParameterNamed:(NSString *)paramName {
+ if (!statement_) return -1;
+ if (hasCFAdditions_) {
+ NSString *cleanedString =
+ [paramName precomposedStringWithCanonicalMapping];
+ if (!cleanedString) return -1;
+ return sqlite3_bind_parameter_index(statement_, [cleanedString UTF8String]);
+ } else {
+ return sqlite3_bind_parameter_index(statement_, [paramName UTF8String]);
+ }
+}
+
+- (NSString *)nameOfParameterAtPosition:(int)position {
+ if ((position < 1) || !statement_) return nil;
+ const char *name = sqlite3_bind_parameter_name(statement_, position);
+ if (!name) return nil;
+ NSString *nameString = [NSString stringWithUTF8String:name];
+ if (hasCFAdditions_) {
+ return [nameString precomposedStringWithCanonicalMapping];
+ } else {
+ return nameString;
+ }
+}
+
+- (int)bindSQLNullAtPosition:(int)position {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_null(statement_, position);
+}
+
+- (int)bindBlobAtPosition:(int)position bytes:(void *)bytes length:(int)length {
+ if (!statement_ || !bytes || !length) return SQLITE_MISUSE;
+ return sqlite3_bind_blob(statement_,
+ position,
+ bytes,
+ length,
+ SQLITE_TRANSIENT);
+}
+
+- (int)bindBlobAtPosition:(int)position data:(NSData *)data {
+ if (!statement_ || !data || !position) return SQLITE_MISUSE;
+ int blobLength = (int)[data length];
+ _GTMDevAssert((blobLength < INT_MAX),
+ @"sqlite methods do not support data lengths "
+ @"exceeding 32 bit sizes");
+ return [self bindBlobAtPosition:position
+ bytes:(void *)[data bytes]
+ length:blobLength];
+}
+
+- (int)bindDoubleAtPosition:(int)position value:(double)value {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_double(statement_, position, value);
+}
+
+- (int)bindNumberAsDoubleAtPosition:(int)position number:(NSNumber *)number {
+ if (!number || !statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_double(statement_, position, [number doubleValue]);
+}
+
+- (int)bindInt32AtPosition:(int)position value:(int)value {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_int(statement_, position, value);
+}
+
+- (int)bindNumberAsInt32AtPosition:(int)position number:(NSNumber *)number {
+ if (!number || !statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_int(statement_, position, [number intValue]);
+}
+
+- (int)bindLongLongAtPosition:(int)position value:(long long)value {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_int64(statement_, position, value);
+}
+
+- (int)bindNumberAsLongLongAtPosition:(int)position number:(NSNumber *)number {
+ if (!number || !statement_) return SQLITE_MISUSE;
+ return sqlite3_bind_int64(statement_, position, [number longLongValue]);
+}
+
+- (int)bindStringAtPosition:(int)position string:(NSString *)string {
+ if (!string || !statement_) return SQLITE_MISUSE;
+ if (hasCFAdditions_) {
+ string = [string precomposedStringWithCanonicalMapping];
+ if (!string) return SQLITE_INTERNAL;
+ }
+ return sqlite3_bind_text(statement_,
+ position,
+ [string UTF8String],
+ -1,
+ SQLITE_TRANSIENT);
+}
+
+#pragma mark Results
+
+- (int)resultColumnCount {
+ if (!statement_) return -1;
+ return sqlite3_column_count(statement_);
+}
+
+- (NSString *)resultColumnNameAtPosition:(int)position {
+ if (!statement_) return nil;
+ const char *name = sqlite3_column_name(statement_, position);
+ if (!name) return nil;
+ NSString *nameString = [NSString stringWithUTF8String:name];
+ if (hasCFAdditions_) {
+ return [nameString precomposedStringWithCanonicalMapping];
+ } else {
+ return nameString;
+ }
+}
+
+- (int)rowDataCount {
+ if (!statement_) return -1;
+ return sqlite3_data_count(statement_);
+}
+
+- (int)resultColumnTypeAtPosition:(int)position {
+ if (!statement_) return -1;
+ return sqlite3_column_type(statement_, position);
+}
+
+- (NSData *)resultBlobDataAtPosition:(int)position {
+ if (!statement_) return nil;
+ const void *bytes = sqlite3_column_blob(statement_, position);
+ int length = sqlite3_column_bytes(statement_, position);
+ if (!(bytes && length)) return nil;
+ return [NSData dataWithBytes:bytes length:length];
+}
+
+- (double)resultDoubleAtPosition:(int)position {
+ if (!statement_) return 0;
+ return sqlite3_column_double(statement_, position);
+}
+
+- (int)resultInt32AtPosition:(int)position {
+ if (!statement_) return 0;
+ return sqlite3_column_int(statement_, position);
+}
+
+- (long long)resultLongLongAtPosition:(int)position {
+ if (!statement_) return 0;
+ return sqlite3_column_int64(statement_, position);
+}
+
+- (NSNumber *)resultNumberAtPosition:(int)position {
+ if (!statement_) return nil;
+ int type = [self resultColumnTypeAtPosition:position];
+ if (type == SQLITE_FLOAT) {
+ // Special case for floats
+ return [NSNumber numberWithDouble:[self resultDoubleAtPosition:position]];
+ } else {
+ // Everything else is cast to int
+ long long result = [self resultLongLongAtPosition:position];
+ return [NSNumber numberWithLongLong:result];
+ }
+}
+
+- (NSString *)resultStringAtPosition:(int)position {
+ if (!statement_) return nil;
+ const char *text = (const char *)sqlite3_column_text(statement_, position);
+ if (!text) return nil;
+ NSString *result = [NSString stringWithUTF8String:text];
+ if (hasCFAdditions_) {
+ return [result precomposedStringWithCanonicalMapping];
+ } else {
+ return result;
+ }
+}
+
+- (id)resultFoundationObjectAtPosition:(int)position {
+ if (!statement_) return nil;
+ int type = [self resultColumnTypeAtPosition:position];
+ id result = nil;
+ switch (type) {
+ case SQLITE_INTEGER:
+ case SQLITE_FLOAT:
+ result = [self resultNumberAtPosition:position];
+ break;
+ case SQLITE_TEXT:
+ result = [self resultStringAtPosition:position];
+ break;
+ case SQLITE_BLOB:
+ result = [self resultBlobDataAtPosition:position];
+ break;
+ case SQLITE_NULL:
+ result = [NSNull null];
+ break;
+ }
+ return result;
+}
+
+- (NSArray *)resultRowArray {
+ int count = [self rowDataCount];
+ if (count < 1) return nil;
+
+ NSMutableArray *finalArray = [NSMutableArray array];
+ for (int i = 0; i < count; i++) {
+ id coldata = [self resultFoundationObjectAtPosition:i];
+ if (!coldata) return nil; // Oops
+ [finalArray addObject:coldata];
+ }
+
+ if (![finalArray count]) return nil;
+ return finalArray;
+}
+
+- (NSDictionary *)resultRowDictionary {
+ int count = [self rowDataCount];
+ if (count < 1) return nil;
+
+ NSMutableDictionary *finalDict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < count; i++) {
+ id coldata = [self resultFoundationObjectAtPosition:i];
+ NSString *colname = [self resultColumnNameAtPosition:i];
+ if (!(coldata && colname)) continue;
+ [finalDict setObject:coldata forKey:colname];
+ }
+ if (![finalDict count]) return nil;
+ return finalDict;
+}
+
+#pragma mark Rows
+
+- (int)stepRow {
+ int rc = SQLITE_BUSY;
+ while (rc == SQLITE_BUSY) {
+ rc = [self stepRowWithTimeout];
+ }
+ return rc;
+}
+
+- (int)stepRowWithTimeout {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_step(statement_);
+}
+
+- (int)reset {
+ if (!statement_) return SQLITE_MISUSE;
+ return sqlite3_reset(statement_);
+}
+
++ (BOOL)isCompleteStatement:(NSString *)statement {
+ BOOL isComplete = NO;
+ if (statement) {
+ isComplete = (sqlite3_complete([statement UTF8String]) ? YES : NO);
+ }
+ return isComplete;
+}
+
++ (NSString*)quoteAndEscapeString:(NSString *)string {
+ char *quoted = sqlite3_mprintf("'%q'", [string UTF8String]);
+ if (!quoted) return nil;
+ NSString *quotedString = [NSString stringWithUTF8String:quoted];
+ sqlite3_free(quoted);
+ return quotedString;
+}
+
+@end