diff options
Diffstat (limited to 'Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m')
-rw-r--r-- | Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m | 413 |
1 files changed, 413 insertions, 0 deletions
diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m new file mode 100644 index 00000000..80b198d5 --- /dev/null +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLResult Categories/Data Conversion.m @@ -0,0 +1,413 @@ +// +// $Id$ +// +// Data Conversion.m +// SPMySQLFramework +// +// Created by Rowan Beentje (rowan.beent.je) on May 26, 2013 +// Copyright (c) 2013 Rowan Beentje. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + + +#import "Data Conversion.h" + +static SPMySQLResultFieldProcessor fieldProcessingMap[256]; +static id NSNullPointer; +static NSStringEncoding NSFromCFStringEncodingBig5; +static NSStringEncoding NSFromCFStringEncodingDOSJapanese; +static NSStringEncoding NSFromCFStringEncodingEUC_KR; +static NSStringEncoding NSFromCFStringEncodingGB_2312_80; +static NSStringEncoding NSFromCFStringEncodingGBK_95; + +@implementation SPMySQLResult (Data_Conversion_Private_API) + +/** + * In the one-off class initialisation, set up the result processing map + */ ++ (void)_initializeDataConversion +{ + // Cached NSNull singleton reference + if (!NSNullPointer) NSNullPointer = [NSNull null]; + + // Go through the list of enum_field_types in mysql_com.h, mapping each to the method for + // processing that result set. + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SHORT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_FLOAT] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DOUBLE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NULL] = SPMySQLResultFieldAsNull; + fieldProcessingMap[MYSQL_TYPE_TIMESTAMP] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_LONGLONG] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_INT24] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_DATETIME] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_YEAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_NEWDATE] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_VARCHAR] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_BIT] = SPMySQLResultFieldAsBit; + fieldProcessingMap[MYSQL_TYPE_NEWDECIMAL] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_ENUM] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_SET] = SPMySQLResultFieldAsString; + fieldProcessingMap[MYSQL_TYPE_TINY_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_MEDIUM_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_LONG_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_BLOB] = SPMySQLResultFieldAsBlob; + fieldProcessingMap[MYSQL_TYPE_VAR_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_STRING] = SPMySQLResultFieldAsStringOrBlob; + fieldProcessingMap[MYSQL_TYPE_GEOMETRY] = SPMySQLResultFieldAsGeometry; + fieldProcessingMap[MYSQL_TYPE_DECIMAL] = SPMySQLResultFieldAsString; + + // Set up string encodings use in if/else checks + NSFromCFStringEncodingBig5 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingBig5); + NSFromCFStringEncodingDOSJapanese = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingDOSJapanese); + NSFromCFStringEncodingEUC_KR = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingEUC_KR); + NSFromCFStringEncodingGB_2312_80 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_2312_80); + NSFromCFStringEncodingGBK_95 = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGBK_95); +} + +/** + * Core data conversion function, taking C data provided by MySQL and converting + * to an appropriate return type. + * Note that the data passed in currently is *not* nul-terminated for fast + * streaming results, which is safe for the current implementation but should be + * kept in mind for future changes. + * If a preview length is supplied, the returned data will be shortened to + * approximately that length, allowing optimisation of data conversion - although + * note only text and data typess will be shortened, and if shortened, will have + * an ellipsis added to indicate truncation. Supply NSNotFound as the length + * to retrieve the entire cell value. + */ +- (id)_getObjectFromBytes:(char *)bytes ofLength:(NSUInteger)length fieldDefinitionIndex:(NSUInteger)fieldIndex previewLength:(NSUInteger)previewLength +{ + MYSQL_FIELD theField = fieldDefinitions[fieldIndex]; + + // A NULL pointer for the data indicates a null value; return a NSNull object. + if (bytes == NULL) { + return NSNullPointer; + } + + // Determine the field processor to use + SPMySQLResultFieldProcessor dataProcessor = _processorForField(theField); + + // If this instance is set to convert all data as strings, override blob processors. + if (returnDataAsStrings && dataProcessor == SPMySQLResultFieldAsBlob) { + dataProcessor = SPMySQLResultFieldAsString; + } + + // Now switch the processing method again to actually process the data. + switch (dataProcessor) { + + // Convert string types using a method that will preserve any nul characters + // within the string + case SPMySQLResultFieldAsString: + case SPMySQLResultFieldAsStringOrBlob: + return _convertStringData(bytes, length, stringEncoding, previewLength); + + // Convert BLOB types to NSData. + // Use the preview length as supplied. + case SPMySQLResultFieldAsBlob: + if (previewLength != NSNotFound && previewLength < length) { + NSMutableData *theData = [NSMutableData dataWithBytes:bytes length:previewLength]; + if (previewLength > 5) { + [theData replaceBytesInRange:NSMakeRange(previewLength - 3, 3) withBytes:"..."]; + } else { + [theData appendBytes:"..." length:3]; + } + return theData; + } + return [NSData dataWithBytes:bytes length:length]; + + // For Geometry types, use a special Geometry object to handle their complexity + case SPMySQLResultFieldAsGeometry: + return [SPMySQLGeometryData dataWithBytes:bytes length:length]; + + // For bit fields, get a zero-padded representation of the data + case SPMySQLResultFieldAsBit: + return _bitStringWithBytes(bytes, length, fieldDefinitions[fieldIndex].length); + + // Convert null types to NSNulls + case SPMySQLResultFieldAsNull: + return NSNullPointer; + + case SPMySQLResultFieldAsUnhandled: + NSLog(@"SPMySQLResult processing encountered an unknown field type (%d), falling back to NSData handling", fieldDefinitions[fieldIndex].type); + return [NSData dataWithBytes:bytes length:length]; + } + + [NSException raise:NSInternalInconsistencyException format:@"Unhandled field type when processing SPMySQLResults"]; + return nil; +} + +/** + * Returns the field processor to use for a specified field. + */ +static inline SPMySQLResultFieldProcessor _processorForField(MYSQL_FIELD aField) +{ + // Determine the default field processor to use + SPMySQLResultFieldProcessor dataProcessor = fieldProcessingMap[aField.type]; + + // Switch the method to process the cell data based on the field type mapping. + switch (dataProcessor) { + + // STRING and VAR_STRING types may be strings or binary types; check the binary flag + case SPMySQLResultFieldAsStringOrBlob: + if (aField.flags & BINARY_FLAG) { + dataProcessor = SPMySQLResultFieldAsBlob; + } + break; + + // Blob types may be automatically be converted to strings, or may be non-binary + case SPMySQLResultFieldAsBlob: + if (!(aField.flags & BINARY_FLAG)) { + dataProcessor = SPMySQLResultFieldAsString; + } + break; + + // In most cases, use the original data processor. + default: + break; + } + + return dataProcessor; +} + +/** + * Provides a binary representation of the supplied bytes as a returned NSString. + * The resulting binary representation will be zero-padded according to the supplied + * field length. + * MySQL stores bit data as string data stored in an 8-bit wide character set. + */ +static inline NSString * _bitStringWithBytes(const char *bytes, NSUInteger length, NSUInteger padLength) +{ + NSUInteger i = 0; + NSUInteger bitLength = length << 3; + + if (bytes == NULL) { + return nil; + } + + // Ensure padLength is never lower than the length + if (padLength < bitLength) { + padLength = bitLength; + } + + // Generate a nul-terminated C string representation of the binary data + char *cStringBuffer = malloc(padLength + 1); + cStringBuffer[padLength] = '\0'; + while (i < bitLength) { + cStringBuffer[padLength - ++i] = ( (bytes[length - 1 - (i >> 3)] >> (i & 0x7)) & 1 ) ? '1' : '0'; + } + while (i++ < padLength) { + cStringBuffer[padLength - i] = '0'; + } + + // Convert to a string + NSString *returnString = [NSString stringWithUTF8String:cStringBuffer]; + + // Free up memory and return + free(cStringBuffer); + return returnString; +} + +/** + * Converts stored string data - which may contain nul bytes - to a native + * Objective-C string, using the current class encoding. + */ +static inline NSString * _convertStringData(const void *dataBytes, NSUInteger dataLength, NSStringEncoding aStringEncoding, NSUInteger previewLength) +{ + + // Fast case - if not using a preview length, or if the data length is shorter, return the requested data. + if (previewLength == NSNotFound || dataLength <= previewLength) { + return [[[NSString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + } + + NSUInteger i = 0, characterLength = 0, byteLength = previewLength; + uint16_t continuationStart, continuationEnd; + + // Handle various special encodings: + + // Variable-length UTF16, in either endianness. Code points U+D800 to U+DFFF are used to + // indicate continuation characters, so can be used to identify if each character is two + // or four bytes long. + if (aStringEncoding == NSUTF16LittleEndianStringEncoding || aStringEncoding == NSUTF16BigEndianStringEncoding) + { + if (aStringEncoding == NSUTF16LittleEndianStringEncoding) { + continuationStart = 0x00D8; + continuationEnd = 0xFFDF; + } else { + continuationStart = 0xD800; + continuationEnd = 0xDFFF; + } + + while (i < dataLength && characterLength < previewLength) { + uint16_t charStart = ((uint16_t *)dataBytes)[i/2]; + if (charStart >= continuationStart && charStart <= continuationEnd) { + i += 4; + } else { + i += 2; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length UTF-8 string encoding. The first bits can be inspected to determine + // character length; one-byte characters start with a zero, two-byte characters with + // 110..., three-byte characters with 1110..., and four-byte with 11110... + else if (aStringEncoding == NSUTF8StringEncoding) + { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart & 0xf0) == 0xf0) { + i += 4; + } else if ((charStart & 0xe0) == 0xe0) { + i += 3; + } else if ((charStart & 0xc0) == 0xc0) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length CP932 encoding; if the first byte is between 0x81-0x9F, + // or between 0xE0-0xFC, the character takes two bytes. + else if (aStringEncoding == NSFromCFStringEncodingDOSJapanese) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart >= 0x81 && charStart <= 0x9f) || (charStart >= 0xE0 && charStart <= 0xFC)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length EUCJPMS encoding, which can be one to three bytes. If a character + // begins with 0x8F, it's three bytes long; if it begins with 0x8E or 0xA1-0xFE, it's + // two bytes long, otherwise only one. + else if (aStringEncoding == NSJapaneseEUCStringEncoding) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if (charStart == 0x8F) { + i += 3; + } else if (charStart == 0x8E || (charStart >= 0xA1 && charStart <= 0xFE)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Variable-length EUC-KR, which can be one or two bytes. If a character begins with + // 0xA1-0xFE, it's two bytes long, otherwise just one byte long. The checks below have + // been modified to look for 0x81-0xFE for two byte logic, for additional compatibility + // with CP949. + // Similarly, variable-length GBK, which can be one or two bytes; a character beginning + // with 0x81-0xFE is two bytes long, otherwise one byte. + else if (aStringEncoding == NSFromCFStringEncodingEUC_KR || aStringEncoding == NSFromCFStringEncodingGBK_95) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if (charStart >= 0x81 && charStart <= 0xFE) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Shift JIS, which can be one or two bytes. A character starting in the ranges + // 0x80-0xA0 or 0xE0-0xFF is two bytes, otherwise one. + else if (aStringEncoding == NSShiftJISStringEncoding) { + while (i < dataLength && characterLength < previewLength) { + uint8_t charStart = ((uint8_t *)dataBytes)[i]; + if ((charStart >= 0x80 && charStart <= 0xA0) || (charStart >= 0xE0 && charStart <= 0xFF)) { + i += 2; + } else { + i++; + } + characterLength++; + } + byteLength = i; + } + + // Encodings where characters are always 4 bytes + else if (aStringEncoding == NSUTF32StringEncoding) + { + characterLength = MIN(previewLength, floor(dataLength / 4)); + byteLength = characterLength * 4; + } + + // Encodings where characters are always 2 bytes + else if ( + aStringEncoding == NSFromCFStringEncodingBig5 || + aStringEncoding == NSFromCFStringEncodingGB_2312_80 || + aStringEncoding == NSUnicodeStringEncoding /* UCS-2 */ + ) { + characterLength = MIN(previewLength, floor(dataLength / 2)); + byteLength = characterLength * 2; + } + + // Default to a single byte per character + else { + characterLength = previewLength; + byteLength = previewLength; + } + + // If returning the full string, use a fast path + if (byteLength >= dataLength) { + return [[[NSString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + } + + // Get a string using the calculated details + NSMutableString *previewString = [[[NSMutableString alloc] initWithBytes:dataBytes length:byteLength encoding:aStringEncoding] autorelease]; + + // If that failed, fall back to using NSString methods to produce a preview + if (!previewString) { + previewString = [[[NSMutableString alloc] initWithBytes:dataBytes length:dataLength encoding:aStringEncoding] autorelease]; + if ([previewString length] > previewLength) { + [previewString deleteCharactersInRange:NSMakeRange(previewLength, [previewString length] - previewLength)]; + } + } + + // Add an indication the string is a preview + [previewString appendString:@"..."]; + + return previewString; +} + + +@end |