From fb15b94b31f88b0ec3c4037ef3f38bc0cc11bbf0 Mon Sep 17 00:00:00 2001 From: Stuart Connolly Date: Sat, 7 Jan 2017 11:57:44 +0000 Subject: Fix #2457: Use system font as default for table content, not Lucida Grande. --- Source/SPAppController.m | 10 +++++++++- Source/SPConstants.h | 1 + Source/SPConstants.m | 1 + Source/SPTablesPreferencePane.m | 6 ++---- 4 files changed, 13 insertions(+), 5 deletions(-) (limited to 'Source') diff --git a/Source/SPAppController.m b/Source/SPAppController.m index 458c62f9..24b981af 100644 --- a/Source/SPAppController.m +++ b/Source/SPAppController.m @@ -104,8 +104,16 @@ */ + (void)initialize { + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + + NSMutableDictionary *preferenceDefaults = [NSMutableDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:SPPreferenceDefaultsFile ofType:@"plist"]]; + + if (![prefs objectForKey:SPGlobalResultTableFont]) { + [preferenceDefaults setObject:[NSArchiver archivedDataWithRootObject:[NSFont systemFontOfSize:11]] forKey:SPGlobalResultTableFont]; + } + // Register application defaults - [[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PreferenceDefaults" ofType:@"plist"]]]; + [prefs registerDefaults:preferenceDefaults]; // Upgrade prefs before any other parts of the app pick up on the values SPApplyRevisionChanges(); diff --git a/Source/SPConstants.h b/Source/SPConstants.h index 26fdc1ab..493e7c5c 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -274,6 +274,7 @@ extern NSString *SPFavoritesDataFile; extern NSString *SPHTMLPrintTemplate; extern NSString *SPHTMLTableInfoPrintTemplate; extern NSString *SPHTMLHelpTemplate; +extern NSString *SPPreferenceDefaultsFile; // SPF file types extern NSString *SPFExportSettingsContentType; diff --git a/Source/SPConstants.m b/Source/SPConstants.m index 16f59f7a..7ae37df3 100644 --- a/Source/SPConstants.m +++ b/Source/SPConstants.m @@ -67,6 +67,7 @@ NSString *SPFavoritesDataFile = @"Favorites.plist"; NSString *SPHTMLPrintTemplate = @"SPPrintTemplate"; NSString *SPHTMLTableInfoPrintTemplate = @"SPTableInfoPrintTemplate"; NSString *SPHTMLHelpTemplate = @"SPMySQLHelpTemplate"; +NSString *SPPreferenceDefaultsFile = @"PreferenceDefaults"; // Folder names NSString *SPThemesSupportFolder = @"Themes"; diff --git a/Source/SPTablesPreferencePane.m b/Source/SPTablesPreferencePane.m index f89849ec..ff3296c2 100644 --- a/Source/SPTablesPreferencePane.m +++ b/Source/SPTablesPreferencePane.m @@ -54,10 +54,8 @@ * Updates the displayed font according to the user's preferences. */ - (void)updateDisplayedTableFontName -{ - NSFont *font = [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]; - - [globalResultTableFontName setFont:font]; +{ + [globalResultTableFontName setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPGlobalResultTableFont]]]; } #pragma mark - -- cgit v1.2.3 From a13b067a5357084b3a7db5472d0e3fec6a26a4cb Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Jan 2017 21:50:21 +0100 Subject: Fix an issue with SQL exports when configured to create an new INSERT statement for every single row (#2671) --- Source/SPSQLExporter.m | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'Source') diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m index 8d53d0b3..e826c36d 100644 --- a/Source/SPSQLExporter.m +++ b/Source/SPSQLExporter.m @@ -99,7 +99,7 @@ SPTableType tableType = SPTableTypeTable; id createTableSyntax = nil; - NSUInteger j, k, t, s, rowCount, queryLength, lastProgressValue, cleanAutoReleasePool = NO; + NSUInteger j, t, s, rowCount, queryLength, lastProgressValue, cleanAutoReleasePool = NO; BOOL sqlOutputIncludeStructure; BOOL sqlOutputIncludeContent; @@ -369,7 +369,8 @@ [self writeUTF8String:[NSString stringWithFormat:@"INSERT INTO %@ (%@)\nVALUES", [tableName backtickQuotedString], [rawColumnNames componentsJoinedAndBacktickQuoted]]]; // Iterate through the rows to construct a VALUES group for each - j = 0, k = 0; + NSUInteger rowsWrittenForTable = 0; + NSUInteger rowsWrittenForCurrentStmt = 0; NSAutoreleasePool *sqlExportPool = [[NSAutoreleasePool alloc] init]; @@ -392,11 +393,8 @@ return; } - j++; - k++; - // Update the progress - NSUInteger progress = (NSUInteger)(j * ([self exportMaxProgress] / rowCount)); + NSUInteger progress = (NSUInteger)((rowsWrittenForTable + 1) * ([self exportMaxProgress] / rowCount)); if (progress > lastProgressValue) { [self setExportProgressValue:progress]; @@ -410,7 +408,7 @@ // Set up the new row as appropriate. If a new INSERT statement should be created, // set one up; otherwise, set up a new row if ((([self sqlInsertDivider] == SPSQLInsertEveryNDataBytes) && (queryLength >= ([self sqlInsertAfterNValue] * 1024))) || - (([self sqlInsertDivider] == SPSQLInsertEveryNRows) && (k == [self sqlInsertAfterNValue]))) + (([self sqlInsertDivider] == SPSQLInsertEveryNRows) && (rowsWrittenForCurrentStmt == [self sqlInsertAfterNValue]))) { [sqlString setString:@";\n\nINSERT INTO "]; [sqlString appendString:[tableName backtickQuotedString]]; @@ -418,12 +416,12 @@ [sqlString appendString:[rawColumnNames componentsJoinedAndBacktickQuoted]]; [sqlString appendString:@")\nVALUES\n\t("]; - queryLength = 0, k = 0; + queryLength = 0, rowsWrittenForCurrentStmt = 0; // Use the opportunity to drain and reset the autorelease pool at the end of this row cleanAutoReleasePool = YES; } - else if (j == 1) { + else if (rowsWrittenForTable == 0) { [sqlString setString:@"\n\t("]; } else { @@ -506,6 +504,9 @@ sqlExportPool = [[NSAutoreleasePool alloc] init]; cleanAutoReleasePool = NO; } + + rowsWrittenForTable++; + rowsWrittenForCurrentStmt++; } // Complete the command -- cgit v1.2.3 From bd75d9a98c695689eedc26b28d68dd6a9d7008b4 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 27 Jan 2017 22:23:57 +0100 Subject: Restrict some variables to their actual usage scope --- Source/SPSQLExporter.m | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'Source') diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m index e826c36d..cb085e39 100644 --- a/Source/SPSQLExporter.m +++ b/Source/SPSQLExporter.m @@ -90,16 +90,13 @@ [sqlTableDataInstance setConnection:connection]; SPMySQLResult *queryResult; - SPMySQLStreamingResult *streamingResult; - NSArray *row; NSString *tableName; NSDictionary *tableDetails; - BOOL *useRawDataForColumnAtIndex, *useRawHexDataForColumnAtIndex; SPTableType tableType = SPTableTypeTable; id createTableSyntax = nil; - NSUInteger j, t, s, rowCount, queryLength, lastProgressValue, cleanAutoReleasePool = NO; + NSUInteger j, s; BOOL sqlOutputIncludeStructure; BOOL sqlOutputIncludeContent; @@ -232,7 +229,7 @@ // Inform the delegate that we are about to start fetcihing data for the current table [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginFetchingData:) withObject:self waitUntilDone:NO]; - lastProgressValue = 0; + NSUInteger lastProgressValue = 0; // Add the name of table [self writeString:[NSString stringWithFormat:@"# %@ %@\n# ------------------------------------------------------------\n\n", NSLocalizedString(@"Dump of table", @"sql export dump of table label"), tableName]]; @@ -297,8 +294,8 @@ NSMutableArray *rawColumnNames = [NSMutableArray arrayWithCapacity:colCount]; NSMutableArray *queryColumnDetails = [NSMutableArray arrayWithCapacity:colCount]; - useRawDataForColumnAtIndex = calloc(colCount, sizeof(BOOL)); - useRawHexDataForColumnAtIndex = calloc(colCount, sizeof(BOOL)); + BOOL *useRawDataForColumnAtIndex = calloc(colCount, sizeof(BOOL)); + BOOL *useRawHexDataForColumnAtIndex = calloc(colCount, sizeof(BOOL)); // Determine whether raw data can be used for each column during processing - safe numbers and hex-encoded data. for (j = 0; j < colCount; j++) @@ -347,17 +344,17 @@ continue; } - rowCount = [NSArrayObjectAtIndex(rowArray, 0) integerValue]; + NSUInteger rowCount = [NSArrayObjectAtIndex(rowArray, 0) integerValue]; if (rowCount) { // Set up a result set in streaming mode - streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT %@ FROM %@", [queryColumnDetails componentsJoinedByString:@", "], [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain]; + SPMySQLStreamingResult *streamingResult = [[connection streamingQueryString:[NSString stringWithFormat:@"SELECT %@ FROM %@", [queryColumnDetails componentsJoinedByString:@", "], [tableName backtickQuotedString]] useLowMemoryBlockingStreaming:([self exportUsingLowMemoryBlockingStreaming])] retain]; // Inform the delegate that we are about to start writing data for the current table [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; - queryLength = 0; + NSUInteger queryLength = 0; // Lock the table for writing and disable keys if supported [metaString setString:@""]; @@ -371,13 +368,15 @@ // Iterate through the rows to construct a VALUES group for each NSUInteger rowsWrittenForTable = 0; NSUInteger rowsWrittenForCurrentStmt = 0; + BOOL cleanAutoReleasePool = NO; NSAutoreleasePool *sqlExportPool = [[NSAutoreleasePool alloc] init]; // Inform the delegate that we are about to start writing the data to disk [delegate performSelectorOnMainThread:@selector(sqlExportProcessWillBeginWritingData:) withObject:self waitUntilDone:NO]; - while ((row = [streamingResult getRowAsArray])) + NSArray *row; + while ((row = [streamingResult getRowAsArray])) { // Check for cancellation flag if ([self isCancelled]) { @@ -428,7 +427,7 @@ [sqlString setString:@",\n\t("]; } - for (t = 0; t < colCount; t++) + for (NSUInteger t = 0; t < colCount; t++) { id object = NSArrayObjectAtIndex(row, t); -- cgit v1.2.3 From ed5194021547e62ba2bd3ef40a287543888ad837 Mon Sep 17 00:00:00 2001 From: Max Lohrmann Date: Sat, 28 Jan 2017 14:26:06 +0100 Subject: Reenable SSH/SSL file picker accessory view by default on OS X 10.11+ (#2673) --- Source/SPConnectionController.m | 12 ++++++++++++ Source/SPConstants.h | 3 +++ 2 files changed, 15 insertions(+) (limited to 'Source') diff --git a/Source/SPConnectionController.m b/Source/SPConnectionController.m index 4a509796..a2159b4f 100644 --- a/Source/SPConnectionController.m +++ b/Source/SPConnectionController.m @@ -71,6 +71,14 @@ static NSString *SPExportFavoritesFilename = @"SequelProFavorites.plist"; @end #endif +#if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_11 +@interface NSOpenPanel (NSOpenPanel_ElCaptian) + +@property (getter=isAccessoryViewDisclosed) BOOL accessoryViewDisclosed; + +@end +#endif + /** * This is a utility function to validate SSL key/certificate files * @param fileData The contents of the file @@ -468,6 +476,10 @@ static NSComparisonResult _compareFavoritesUsingKey(id favorite1, id favorite2, keySelectionPanel = [[NSOpenPanel openPanel] retain]; // retain/release needed on OS X ≤ 10.6 according to Apple doc [keySelectionPanel setShowsHiddenFiles:[prefs boolForKey:SPHiddenKeyFileVisibilityKey]]; [keySelectionPanel setAccessoryView:accessoryView]; + //on os x 10.11+ the accessory view will be hidden by default and has to be made visible + if(accessoryView && [keySelectionPanel respondsToSelector:@selector(setAccessoryViewDisclosed:)]) { + [keySelectionPanel setAccessoryViewDisclosed:YES]; + } [keySelectionPanel setDelegate:self]; [keySelectionPanel beginSheetModalForWindow:[dbDocument parentWindow] completionHandler:^(NSInteger returnCode) { diff --git a/Source/SPConstants.h b/Source/SPConstants.h index 493e7c5c..161b5b0b 100644 --- a/Source/SPConstants.h +++ b/Source/SPConstants.h @@ -675,6 +675,9 @@ void _SPClear(id *addr); #ifndef __MAC_10_10 #define __MAC_10_10 101000 #endif +#ifndef __MAC_10_11 +#define __MAC_10_11 101100 +#endif // This enum is available since 10.5 but only got a "name" in 10.10 #if __MAC_OS_X_VERSION_MAX_ALLOWED < __MAC_10_10 -- cgit v1.2.3 From 325b447afff5bb1ae4fa898f495a0f78d268e866 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 28 Jan 2017 19:21:25 +0100 Subject: Add a warning when Sequel Pro was launched from Terminal and a SSH connection is made --- Source/SPSSHTunnel.m | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) (limited to 'Source') diff --git a/Source/SPSSHTunnel.m b/Source/SPSSHTunnel.m index 390c516c..1306b16f 100644 --- a/Source/SPSSHTunnel.m +++ b/Source/SPSSHTunnel.m @@ -383,6 +383,7 @@ static unsigned short getRandomPort(); } else { TA(@"-L", ([NSString stringWithFormat:@"%ld:%@:%ld", (long)localPort, remoteHost, (long)remotePort])); } +#undef TA [task setArguments:taskArguments]; @@ -414,10 +415,54 @@ static unsigned short getRandomPort(); [task setStandardError:standardError]; [[ NSNotificationCenter defaultCenter] addObserver:self selector:@selector(standardErrorHandler:) - name:@"NSFileHandleDataAvailableNotification" + name:NSFileHandleDataAvailableNotification object:[standardError fileHandleForReading]]; [[standardError fileHandleForReading] waitForDataInBackgroundAndNotify]; + { + static BOOL hasCheckedTTY = NO; + if(!hasCheckedTTY) { + int fd = open("/dev/tty", O_RDWR); + if(fd >= 0) { + close(fd); + fprintf(stderr, ( + "!!!\n" + "!!! You are running Sequel Pro from a TTY.\n" + "!!! Any SSH connections that require user input (e.g. a password/passphrase) will fail\n" + "!!! and appear stalled indefinitely.\n" + "!!! Sorry!\n" + "!!!\n" + )); + fflush(stderr); + // Explanation: + // OpenSSH by default requests passwords AND yes/no questions directly from the TTY, + // if it is part of a session group that has a controlling terminal (which is the case for + // processes created by Terminal.app). + // + // But this won't work, because only the foreground process group can read from /dev/tty and + // NSTask will create a new (background) process group for OpenSSH on launch. + // Side note: The internal method called from -[NSTask launch] + // -[NSConcreteTask launchWithDictionary:] accepts key @"_NSTaskNoNewProcessGroup" to skip that. + // + // Now, there are two preconditions for OpenSSH to use our SSH_ASKPASS utility instead: + // 1) The "DISPLAY" envvar has to be set + // 2) There must be no controlling terminal (ie. open("/dev/tty") fails) + // (See readpass.c#read_passphrase() in OpenSSH for the relevant code) + // + // -[NSTask launch] internally uses posix_spawn() and according to its documentation + // "The new process also inherits the following attributes from the calling + // process: [...] control terminal [...]" + // So if we wanted to avoid that, we would have to reimplement the whole NSTask class + // and use fork()+exec*()+setsid() instead (or use GNUStep's NSTask which already does this). + // + // We could also do ioctl(fd, TIOCNOTTY, 0); before launching the child process, but + // changing our own controlling terminal does not seem like a good idea in the middle + // of the application lifecycle, when we don't know what other Cocoa code may use it... + } + hasCheckedTTY = YES; + } + } + @try { // Launch and run the tunnel [task launch]; //throws for invalid paths, missing +x permission -- cgit v1.2.3 From 2c3a59464192a1f942c36fcc08d1b452eca3f059 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Feb 2017 21:11:14 +0100 Subject: Fix display of column type in FieldEditorController for JSON type (part of #2199) --- Source/SPFieldEditorController.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'Source') diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m index 8c5c72ae..a32f32dc 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -249,7 +249,8 @@ typedef enum { if ([fieldType length]) [label appendString:fieldType]; - if (maxTextLength > 0) + //skip length for JSON type since it's a constant and MySQL doesn't display it either + if (maxTextLength > 0 && ![[fieldType uppercaseString] isEqualToString:SPMySQLJsonType]) [label appendFormat:@"(%lld) ", maxTextLength]; if (!_allowNULL) -- cgit v1.2.3 From ffe8c1326860b9510619f0efbbfe97be746c6956 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 12 Feb 2017 03:59:59 +0100 Subject: Fix a minor memory leak --- Source/SPFieldEditorController.m | 1 + 1 file changed, 1 insertion(+) (limited to 'Source') diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m index a32f32dc..07d3980f 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -195,6 +195,7 @@ typedef enum { } #endif + [self setEditedFieldInfo:nil]; if ( sheetEditData ) SPClear(sheetEditData); #ifndef SP_CODA if ( qlTypes ) SPClear(qlTypes); -- cgit v1.2.3 From 618e84a46786b90f731ad963c5e14ea78dcfe58e Mon Sep 17 00:00:00 2001 From: Max Lohrmann Date: Sun, 12 Feb 2017 18:33:06 +0100 Subject: * Add a JSON formatter * MySQL JSON type columns are now automatically formatted when opening them in the Field Editor --- Source/SPFieldEditorController.h | 1 + Source/SPFieldEditorController.m | 26 ++- Source/SPJSONFormatter.h | 123 +++++++++++++ Source/SPJSONFormatter.m | 364 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 511 insertions(+), 3 deletions(-) create mode 100644 Source/SPJSONFormatter.h create mode 100644 Source/SPJSONFormatter.m (limited to 'Source') diff --git a/Source/SPFieldEditorController.h b/Source/SPFieldEditorController.h index ec36c9d2..0c0d0a0e 100644 --- a/Source/SPFieldEditorController.h +++ b/Source/SPFieldEditorController.h @@ -188,6 +188,7 @@ NSInteger editSheetReturnCode; BOOL _isGeometry; + BOOL _isJSON; NSUndoManager *esUndoManager; NSDictionary *editedFieldInfo; diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m index a32f32dc..19668d74 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -37,6 +37,7 @@ #include #import "SPCustomQuery.h" #import "SPTableContent.h" +#import "SPJSONFormatter.h" #import @@ -237,6 +238,7 @@ typedef enum { callerInstance = sender; _isGeometry = ([[fieldType uppercaseString] isEqualToString:@"GEOMETRY"]) ? YES : NO; + _isJSON = ([[fieldType uppercaseString] isEqualToString:SPMySQLJsonType]); // Set field label NSMutableString *label = [NSMutableString string]; @@ -250,7 +252,7 @@ typedef enum { [label appendString:fieldType]; //skip length for JSON type since it's a constant and MySQL doesn't display it either - if (maxTextLength > 0 && ![[fieldType uppercaseString] isEqualToString:SPMySQLJsonType]) + if (maxTextLength > 0 && !_isJSON) [label appendFormat:@"(%lld) ", maxTextLength]; if (!_allowNULL) @@ -353,7 +355,8 @@ typedef enum { encoding = anEncoding; - _isBlob = isFieldBlob; + // we don't want the hex/image controls for JSON + _isBlob = (!_isJSON && isFieldBlob); BOOL isBinary = ([[fieldType uppercaseString] isEqualToString:@"BINARY"] || [[fieldType uppercaseString] isEqualToString:@"VARBINARY"]); @@ -443,7 +446,18 @@ typedef enum { [editTextScrollView setHidden:NO]; } else { - stringValue = [sheetEditData retain]; + // If the input is a JSON type column we can format it. + // Since MySQL internally stores JSON in binary, it does not retain any formatting + do { + if(_isJSON) { + NSString *formatted = [SPJSONFormatter stringByFormattingString:sheetEditData]; + if(formatted) { + stringValue = [formatted retain]; + break; + } + } + stringValue = [sheetEditData retain]; + } while(0); [hexTextView setString:@""]; @@ -652,6 +666,12 @@ typedef enum { if(callerInstance) { id returnData = ( editSheetReturnCode && _isEditable ) ? (_isGeometry) ? [editTextView string] : sheetEditData : nil; + //for MySQLs JSON type remove the formatting again, since it won't be stored anyway + if(_isJSON) { + NSString *unformatted = [SPJSONFormatter stringByUnformattingString:returnData]; + if(unformatted) returnData = unformatted; + } + #ifdef SP_CODA /* patch */ if ( [callerInstance isKindOfClass:[SPCustomQuery class]] ) [(SPCustomQuery*)callerInstance processFieldEditorResult:returnData contextInfo:contextInfo]; diff --git a/Source/SPJSONFormatter.h b/Source/SPJSONFormatter.h new file mode 100644 index 00000000..fd0b7afd --- /dev/null +++ b/Source/SPJSONFormatter.h @@ -0,0 +1,123 @@ +// +// SPJSONFormatter.h +// sequel-pro +// +// Created by Max Lohrmann on 10.02.17. +// Copyright (c) 2017 Max Lohrmann. 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 + +#import + +typedef NS_ENUM(UInt8, SPJSONToken) { + JSON_TOK_EOF, + JSON_TOK_CURLY_BRACE_OPEN, + JSON_TOK_CURLY_BRACE_CLOSE, + JSON_TOK_SQUARE_BRACE_OPEN, + JSON_TOK_SQUARE_BRACE_CLOSE, + JSON_TOK_DOUBLE_QUOTE, + JSON_TOK_COLON, + JSON_TOK_COMMA, + JSON_TOK_OTHER, + JSON_TOK_STRINGDATA +}; + +typedef NS_ENUM(UInt8, SPJSONContext) { + JSON_ROOT_CONTEXT, + JSON_STRING_CONTEXT +}; + +typedef struct { + const char *str; + size_t len; + size_t pos; + SPJSONContext ctxt; +} SPJSONTokenizerState; + +typedef struct { + SPJSONToken tok; + size_t pos; + size_t len; +} SPJSONTokenInfo; + +/** + * Initializes a caller defined SPJSONTokenizerState structure to the string that is passed. + * The string is not retained. The caller is responsible for making sure it stays around as long + * as the tokenizer is used! + * + * @return 0 on success, -1 if an argument was NULL. + */ +int SPJSONTokenizerInit(NSString *input, SPJSONTokenizerState *stateInfo); + +/** + * This function returns the token that is at the current position of the input string or following + * it most closely and forward the input string accordingly. + * + * The JSON_TOK_EOF token is a zero length token that is returned after the last character in the input + * string has been read and tokenized. Any call to this function after JSON_TOK_EOF has been returned + * will return the same. + * + * JSON_TOK_OTHER and JSON_TOK_STRINGDATA are variable length tokens (but never 0) that represent whitespace, + * numbers, true/false/null and the contents of strings (without the double quotes). + * + * The remaining tokens correspond to the respective control characters in JSON and are always a single + * character long. + * + * The token/position/length information will be assigned to the tokenMatch argument given by the caller. + * + * @return 1 If a token was successfully matched + * 0 If the matched token was JSON_TOK_EOF (tokenMatch will still be set, like for 1) + * -1 If the passed arguments were invalid (tokenMatch will not be updated) + * + * DO NOT try to build a parser/syntax validator based on this code! It is much too lenient for those purposes! + */ +int SPJSONTokenizerGetNextToken(SPJSONTokenizerState *stateInfo, SPJSONTokenInfo *tokenMatch); + + +@interface SPJSONFormatter : NSObject + +/** + * This method will return a formatted copy of the input string. + * + * - A line break is inserted after every ",". + * - There will be a line break after every "{" and "[" (except if they are empty) and the indent + * of the following lines is increased by 1. + * - There will be a line break before "]" and "}" (except if they are empty) and the indent of this line + * and the following lines will be decreased by 1. + * - A line break will be inserted after "]" and "}", except if a "," follows. + * - Indenting is done using a single "\t" character per level. + * + * @return The formatted string or nil if formatting failed. + */ ++ (NSString *)stringByFormattingString:(NSString *)input; + +/** + * This method will return a compact copy of the input string. + * All whitespace (outside of strings) will be removed (except for a single space after ":" and ",") + * + * @return The unformatted string or nil if unformatting failed. + */ ++ (NSString *)stringByUnformattingString:(NSString *)input; + +@end diff --git a/Source/SPJSONFormatter.m b/Source/SPJSONFormatter.m new file mode 100644 index 00000000..05cc2992 --- /dev/null +++ b/Source/SPJSONFormatter.m @@ -0,0 +1,364 @@ +// +// SPJSONFormatter.m +// sequel-pro +// +// Created by Max Lohrmann on 10.02.17. +// Copyright (c) 2017 Max Lohrmann. 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 + +#import "SPJSONFormatter.h" + + +static char GetNextANSIChar(SPJSONTokenizerState *stateInfo); + + +@implementation SPJSONFormatter + ++ (NSString *)stringByFormattingString:(NSString *)input +{ + SPJSONTokenizerState stateInfo; + if(SPJSONTokenizerInit(input,&stateInfo) == -1) return nil; + + NSUInteger idLevel = 0; + + NSCharacterSet *wsNlCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSMutableString *formatted = [[NSMutableString alloc] init]; + + SPJSONToken prevTokenType = JSON_TOK_EOF; + SPJSONTokenInfo curToken; + if(SPJSONTokenizerGetNextToken(&stateInfo,&curToken) == -1) { + [formatted release]; + return nil; + } + + BOOL needIndent = NO; + SPJSONTokenInfo nextToken; + do { + //we need to know the next token to do meaningful formatting + if(SPJSONTokenizerGetNextToken(&stateInfo,&nextToken) == -1) { + [formatted release]; + return nil; + } + + if(curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE) + idLevel--; + + //if this token is a "]" or "}" and there was no "[" or "{" directly before it, add a linebreak before + if(prevTokenType != JSON_TOK_CURLY_BRACE_OPEN && prevTokenType != JSON_TOK_SQUARE_BRACE_OPEN && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE)) { + [formatted appendString:@"\n"]; + needIndent = YES; + } + + //if this token is on a new line indent it + if(needIndent && idLevel > 0) { + //32 tabs pool (with fallback for even deeper nesting) + static NSString *tabs = @"\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + NSUInteger myIdLevel = idLevel; + while(myIdLevel > [tabs length]) { + [formatted appendString:tabs]; + myIdLevel -= [tabs length]; + } + [formatted appendString:[tabs substringWithRange:NSMakeRange(0, myIdLevel)]]; + needIndent = NO; + } + + //save ourselves the overhead of creating an NSString if we already know what it will contain + NSString *curTokenString; + id freeMe = nil; + switch (curToken.tok) { + case JSON_TOK_CURLY_BRACE_OPEN: + curTokenString = @"{"; + break; + + case JSON_TOK_CURLY_BRACE_CLOSE: + curTokenString = @"}"; + break; + + case JSON_TOK_SQUARE_BRACE_OPEN: + curTokenString = @"["; + break; + + case JSON_TOK_SQUARE_BRACE_CLOSE: + curTokenString = @"]"; + break; + + case JSON_TOK_DOUBLE_QUOTE: + curTokenString = @"\""; + break; + + case JSON_TOK_COLON: + curTokenString = @": "; //add a space after ":" for readability + break; + + case JSON_TOK_COMMA: + curTokenString = @","; + break; + + //JSON_TOK_OTHER + //JSON_TOK_STRINGDATA + default: + curTokenString = [[NSString alloc] initWithBytesNoCopy:(void *)(&stateInfo.str[curToken.pos]) length:curToken.len encoding:NSUTF8StringEncoding freeWhenDone:NO]; + //for everything except strings get rid of surrounding whitespace + if(curToken.tok != JSON_TOK_STRINGDATA) { + NSString *newTokenString = [[curTokenString stringByTrimmingCharactersInSet:wsNlCharset] retain]; + [curTokenString release]; + curTokenString = newTokenString; + } + freeMe = curTokenString; + } + + [formatted appendString:curTokenString]; + + if(freeMe) [freeMe release]; + + //if the current token is a "[", "{" or "," and the next token is not a "]" or "}" add a line break afterwards + if( + curToken.tok == JSON_TOK_COMMA || + (curToken.tok == JSON_TOK_CURLY_BRACE_OPEN && nextToken.tok != JSON_TOK_CURLY_BRACE_CLOSE) || + (curToken.tok == JSON_TOK_SQUARE_BRACE_OPEN && nextToken.tok != JSON_TOK_SQUARE_BRACE_CLOSE) + ) { + [formatted appendString:@"\n"]; + needIndent = YES; + } + + if(curToken.tok == JSON_TOK_CURLY_BRACE_OPEN || curToken.tok == JSON_TOK_SQUARE_BRACE_OPEN) + idLevel++; + + prevTokenType = curToken.tok; + curToken = nextToken; + } while(curToken.tok != JSON_TOK_EOF); //SPJSONTokenizerGetNextToken() will always return JSON_TOK_EOF once it has reached that state + + return [formatted autorelease]; +} + ++ (NSString *)stringByUnformattingString:(NSString *)input +{ + SPJSONTokenizerState stateInfo; + if(SPJSONTokenizerInit(input,&stateInfo) == -1) return nil; + + NSCharacterSet *wsNlCharset = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSMutableString *unformatted = [[NSMutableString alloc] init]; + + do { + SPJSONTokenInfo curToken; + if(SPJSONTokenizerGetNextToken(&stateInfo,&curToken) == -1) { + [unformatted release]; + return nil; + } + + if(curToken.tok == JSON_TOK_EOF) break; + + //save ourselves the overhead of creating an NSString from input if we already know what it will contain + NSString *curTokenString; + id freeMe = nil; + switch (curToken.tok) { + case JSON_TOK_CURLY_BRACE_OPEN: + curTokenString = @"{"; + break; + + case JSON_TOK_CURLY_BRACE_CLOSE: + curTokenString = @"}"; + break; + + case JSON_TOK_SQUARE_BRACE_OPEN: + curTokenString = @"["; + break; + + case JSON_TOK_SQUARE_BRACE_CLOSE: + curTokenString = @"]"; + break; + + case JSON_TOK_DOUBLE_QUOTE: + curTokenString = @"\""; + break; + + case JSON_TOK_COLON: + curTokenString = @": "; //add a space after ":" to match MySQL + break; + + case JSON_TOK_COMMA: + curTokenString = @", "; //add a space after "," to match MySQL + break; + + //JSON_TOK_OTHER + //JSON_TOK_STRINGDATA + default: + curTokenString = [[NSString alloc] initWithBytesNoCopy:(void *)(&stateInfo.str[curToken.pos]) length:curToken.len encoding:NSUTF8StringEncoding freeWhenDone:NO]; + //for everything except strings get rid of surrounding whitespace + if(curToken.tok != JSON_TOK_STRINGDATA) { + NSString *newTokenString = [[curTokenString stringByTrimmingCharactersInSet:wsNlCharset] retain]; + [curTokenString release]; + curTokenString = newTokenString; + } + freeMe = curTokenString; + } + + [unformatted appendString:curTokenString]; + + if(freeMe) [freeMe release]; + + } while(1); + + return [unformatted autorelease]; +} + + +@end + +/** + * This function returns the char at the current position in the input string and forwards the read pointer to the next char. + * If the character is part of an UTF8 multibyte sequence, the function will skip forward until a single byte character is found again + * or EOF is reached (whichever comes first). + * + * stateInfo MUST be valid or this will crash! + * + * @return Either a char in the range 0-127 or -1 if EOF is reached. + */ +char GetNextANSIChar(SPJSONTokenizerState *stateInfo) { + do { + if(stateInfo->pos >= stateInfo->len) + return -1; + char val = stateInfo->str[stateInfo->pos++]; + // all utf8 multibyte characters start with the most significant bit being 1 for all of their bytes + // but since all JSON control characters are in the single byte ANSI compatible plane, we can just ignore any MB chars + if((val & 0x80) == 0) + return val; + } while(1); +} + +int SPJSONTokenizerInit(NSString *input, SPJSONTokenizerState *stateInfo) { + if(!input || ![input respondsToSelector:@selector(UTF8String)] || stateInfo == NULL) + return -1; + + stateInfo->ctxt = JSON_ROOT_CONTEXT; + stateInfo->pos = 0; + stateInfo->str = [input UTF8String]; + stateInfo->len = strlen(stateInfo->str); //we deem -[NSString UTF8String] to be a safe source + + return 0; +} + +int SPJSONTokenizerGetNextToken(SPJSONTokenizerState *stateInfo, SPJSONTokenInfo *tokenMatch) { + if(tokenMatch == NULL || stateInfo == NULL || stateInfo->str == NULL) + return -1; + + size_t posBefore = stateInfo->pos; + do { + char c = GetNextANSIChar(stateInfo); + if(stateInfo->ctxt == JSON_STRING_CONTEXT) { + //the only characters inside a string that are relevant to us are backslash and doublequote + if(c == '"' || c == -1) { + //if the string has contents, return that first + if((stateInfo->pos - posBefore) > 1) { + tokenMatch->tok = JSON_TOK_STRINGDATA; + tokenMatch->pos = posBefore; + if(c == '"') + stateInfo->pos--; //rewind to read it again + tokenMatch->len = stateInfo->pos - posBefore; + return 1; + } + //string is terminated by EOF (invalid JSON) + if(c == -1) { + //switch to root context and try again to reach EOF branch below + stateInfo->ctxt = JSON_ROOT_CONTEXT; + continue; + } + stateInfo->ctxt = JSON_ROOT_CONTEXT; + tokenMatch->tok = JSON_TOK_DOUBLE_QUOTE; + tokenMatch->pos = posBefore; + tokenMatch->len = stateInfo->pos - posBefore; + return 1; + } + else if(c == '\\') { + //for backslash we need to skip the next byte + // We don't care for the value of the next byte since we don't really want to parse JSON, but only format it. + // Thus we only have to pay attention to differntiate backslash-dquote and dquote. + stateInfo->pos++; + } + } + else if(c == -1) { + //if there is still unreturned input, return that first + if(posBefore < stateInfo->len) { + tokenMatch->tok = JSON_TOK_OTHER; + tokenMatch->pos = posBefore; + tokenMatch->len = stateInfo->pos - posBefore; + return 1; + } + tokenMatch->tok = JSON_TOK_EOF; + tokenMatch->pos = stateInfo->pos; //EOF sits after the last character + tokenMatch->len = 0; // EOF has no length + return 0; + } + else { + SPJSONToken tokFound = JSON_TOK_EOF; + + switch(c) { + case '"': + stateInfo->ctxt = JSON_STRING_CONTEXT; + tokFound = JSON_TOK_DOUBLE_QUOTE; + break; + + case '{': + tokFound = JSON_TOK_CURLY_BRACE_OPEN; + break; + + case '}': + tokFound = JSON_TOK_CURLY_BRACE_CLOSE; + break; + + case '[': + tokFound = JSON_TOK_SQUARE_BRACE_OPEN; + break; + + case ']': + tokFound = JSON_TOK_SQUARE_BRACE_CLOSE; + break; + + case ':': + tokFound = JSON_TOK_COLON; + break; + + case ',': + tokFound = JSON_TOK_COMMA; + break; + } + + //if we found a token, but had to walk more than 1 char there was something else + //between the previous token and this token, which we should report first + if(tokFound != JSON_TOK_EOF && (stateInfo->pos - posBefore) > 1) { + stateInfo->ctxt = JSON_ROOT_CONTEXT; + stateInfo->pos--; //rewind so we will read the token again next time + tokFound = JSON_TOK_OTHER; + } + + if(tokFound != JSON_TOK_EOF) { + tokenMatch->tok = tokFound; + tokenMatch->pos = posBefore; + tokenMatch->len = stateInfo->pos - posBefore; + return 1; + } + } + } while(1); +} -- cgit v1.2.3 From aee6a009a5355aa3c7c91f885685868e5294e71c Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 12 Feb 2017 19:06:31 +0100 Subject: Fix an erroneous if condition (#2688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some rare cases this could have resulted in an unexpected „No data was updated“ error message --- Source/SPFieldEditorController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Source') diff --git a/Source/SPFieldEditorController.m b/Source/SPFieldEditorController.m index 21a220c7..0298973d 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -1380,7 +1380,7 @@ typedef enum { if([notification object] == editTextView) { // Do nothing if user really didn't changed text (e.g. for font size changing return) if(!editTextViewWasChanged && (editSheetWillBeInitialized - || (([[[notification object] textStorage] editedRange].length == 0) + || (([[[notification object] textStorage] editedRange].location == NSNotFound) && ([[[notification object] textStorage] changeInLength] == 0)))) { // Inform the undo-grouping about the caret movement selectionChanged = YES; -- cgit v1.2.3 From d2b1a5b84cb295eba8617f7e80681e0eeca46f0d Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 12 Feb 2017 20:49:31 +0100 Subject: Add unit tests and fix a bug in JSON formatter --- Source/SPJSONFormatter.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'Source') diff --git a/Source/SPJSONFormatter.m b/Source/SPJSONFormatter.m index 05cc2992..258845c9 100644 --- a/Source/SPJSONFormatter.m +++ b/Source/SPJSONFormatter.m @@ -62,11 +62,11 @@ static char GetNextANSIChar(SPJSONTokenizerState *stateInfo); return nil; } - if(curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE) + if(idLevel > 0 && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE)) idLevel--; - //if this token is a "]" or "}" and there was no "[" or "{" directly before it, add a linebreak before - if(prevTokenType != JSON_TOK_CURLY_BRACE_OPEN && prevTokenType != JSON_TOK_SQUARE_BRACE_OPEN && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE)) { + //if this token is a "]" or "}" and there was no ",", "[" or "{" directly before it, add a linebreak before + if(prevTokenType != JSON_TOK_CURLY_BRACE_OPEN && prevTokenType != JSON_TOK_SQUARE_BRACE_OPEN && prevTokenType != JSON_TOK_COMMA && (curToken.tok == JSON_TOK_SQUARE_BRACE_CLOSE || curToken.tok == JSON_TOK_CURLY_BRACE_CLOSE)) { [formatted appendString:@"\n"]; needIndent = YES; } @@ -255,7 +255,7 @@ int SPJSONTokenizerInit(NSString *input, SPJSONTokenizerState *stateInfo) { stateInfo->ctxt = JSON_ROOT_CONTEXT; stateInfo->pos = 0; stateInfo->str = [input UTF8String]; - stateInfo->len = strlen(stateInfo->str); //we deem -[NSString UTF8String] to be a safe source + stateInfo->len = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; return 0; } -- cgit v1.2.3