diff options
-rw-r--r-- | Frameworks/PostgresKit/README.md (renamed from Frameworks/PostgresKit/README) | 29 | ||||
-rw-r--r-- | Frameworks/SPMySQLFramework/LICENSE | 26 | ||||
-rw-r--r-- | Frameworks/SPMySQLFramework/README.md | 43 | ||||
-rw-r--r-- | Frameworks/SPMySQLFramework/Readme.txt | 69 | ||||
-rw-r--r-- | Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h | 1 | ||||
-rw-r--r-- | Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m | 67 | ||||
-rw-r--r-- | Resources/Plists/PreferenceDefaults.plist | 2 | ||||
-rw-r--r-- | Source/SPAppController.m | 10 | ||||
-rw-r--r-- | Source/SPConnectionController.m | 12 | ||||
-rw-r--r-- | Source/SPConstants.h | 4 | ||||
-rw-r--r-- | Source/SPConstants.m | 1 | ||||
-rw-r--r-- | Source/SPFieldEditorController.h | 1 | ||||
-rw-r--r-- | Source/SPFieldEditorController.m | 30 | ||||
-rw-r--r-- | Source/SPJSONFormatter.h | 123 | ||||
-rw-r--r-- | Source/SPJSONFormatter.m | 364 | ||||
-rw-r--r-- | Source/SPSQLExporter.m | 40 | ||||
-rw-r--r-- | Source/SPSSHTunnel.m | 47 | ||||
-rw-r--r-- | Source/SPTablesPreferencePane.m | 6 | ||||
-rw-r--r-- | UnitTests/SPJSONFormatterTests.m | 141 | ||||
-rw-r--r-- | readme.md | 4 | ||||
-rw-r--r-- | sequel-pro.xcodeproj/project.pbxproj | 18 |
21 files changed, 893 insertions, 145 deletions
diff --git a/Frameworks/PostgresKit/README b/Frameworks/PostgresKit/README.md index d4cdefea..063dacc4 100644 --- a/Frameworks/PostgresKit/README +++ b/Frameworks/PostgresKit/README.md @@ -1,10 +1,9 @@ -POSTGRESKIT README ------------------- +# PostgresKit PostgresKit is a fork and heavily modified version of the PostgresClientKit code from the PostgresKit project: - http://code.google.com/p/postgres-kit/ +[http://code.google.com/p/postgres-kit/](http://code.google.com/p/postgres-kit/) PostgresClientKit was originally written by David Thorpe and is licensed under version 2 of the Apache License. @@ -12,7 +11,9 @@ version 2 of the Apache License. This PostgresKit fork was created by Stuart Connolly on July 22, 2012 and is to be developed as part of the Sequel Pro project: - http://sequelpro.com/ +[http://sequelpro.com/](http://sequelpro.com/) + +## License Any new code added during it's development is licensed under the MIT license and is copyrighted by the respective developer and the Sequel Pro team. @@ -28,7 +29,7 @@ libpq is licensed under The PostgreSQL License and is copyrighted by: Full license: - http://www.postgresql.org/about/licence/ +[https://www.postgresql.org/about/licence/](https://www.postgresql.org/about/licence/) libpqtypes is licensed under the BSD license and is copyrighted by: @@ -36,14 +37,13 @@ libpqtypes is licensed under the BSD license and is copyrighted by: Full License: - http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE +[http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE](http://libpqtypes.esilo.com/pkgdocs.html?file=LICENSE) The entire framework is dual licensed under both version 2 of the Apache license and the MIT license. Use of it must carry both of the following licenses to indicate this: - -APACHE 2 LICENSE +### Apache 2 License Copyright (c) 2008-2009 David Thorpe, djt@mutablelogic.com @@ -51,7 +51,7 @@ 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 +[https://www.apache.org/licenses/LICENSE-2.0](https://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, @@ -59,11 +59,10 @@ 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. +### MIT License -MIT LICENSE - -Copyright (c) 2012 Sequel Pro Team. All rights reserved. - +Copyright (c) 2017 Sequel Pro Team. 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 @@ -72,10 +71,10 @@ 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 diff --git a/Frameworks/SPMySQLFramework/LICENSE b/Frameworks/SPMySQLFramework/LICENSE new file mode 100644 index 00000000..fe855559 --- /dev/null +++ b/Frameworks/SPMySQLFramework/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2017 Rowan Beentje (rowan.beent.je) and the Sequel Pro team. + +All rights reserved. + +http://sequelpro.com/ + +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. diff --git a/Frameworks/SPMySQLFramework/README.md b/Frameworks/SPMySQLFramework/README.md new file mode 100644 index 00000000..f65141d0 --- /dev/null +++ b/Frameworks/SPMySQLFramework/README.md @@ -0,0 +1,43 @@ +# SPMySQL.framework + +The SPMySQL Framework is intended to provide a stable MySQL connection framework, with the ability to run text-based queries and rapidly retrieve result sets with conversion from MySQL data types to Cocoa objects. + +SPMySQL.framework has an interface loosely based around that provided by MCPKit by Serge Cohen and Bertrand Mansion ([http://mysql-cocoa.sourceforge.net/](http://mysql-cocoa.sourceforge.net/)), and in particular the heavily modified Sequel Pro version ([http://www.sequelpro.com/](http://www.sequelpro.com/)). It is a full rewrite of the original framework, although it includes code from patches implementing the following Sequel Pro functionality, largely contributed by Hans-Jörg Bibiko, Stuart Connolly, Jakob Egger and Rowan Beentje: + +* Connection locking (Jakob et al.) +* Ping & keepalive (Rowan et al.) +* Query cancellation (Rowan et al.) +* Delegate setup (Stuart et al.) +* SSL support (Rowan et al.) +* Connection checking (Rowan et al.) +* Version state (Stuart et al.) +* Maximum packet size control (Hans et al.) +* Result multithreading and streaming (Rowan et al.) +* Improved encoding support & switching (Rowan et al.) +* Database structure; moved to inside the app (Hans et al.) +* Query reattempts and error-handling approach (Rowan et al.) +* Geometry result class (Hans et al.) +* Connection proxy (Stuart et al.) + +## Integration + +SPMySQL.framework can be added to your project as a standard Cocoa framework, or the entire project +can be added as a subproject in Xcode. + +To add as a subproject in Xcode: + +1. Add the SPMySQL framework's `.xcodeproj` to your current project +2. Choose an existing target, Get Info, and under direct dependenies add a new dependency. Choose the SPMySQL.framework target from the sub-project. +3. Expand the subproject to see its child target - SPMySQL.framework. Drag this to the "Link Binary With Libraries" build phase of any targets using the framework. +4. If you don't have a Copy Frameworks phase, add one; drag the SPMySQL.framework child target to this phase. +5. In your build settings, add a User Header Search Path; make it a recursive path to the SPMySQL project folder location (for example `${PROJECT_DIR}/Frameworks/SPMySQLFramework`). This should allow you to `#include "SPMySQL.h"` and have everything function. + +As a last resort jump onto IRC and join #sequel-pro on irc.freenode.net and any of the +developers will be more than happy to help you out. + +## License + +Copyright (c) 2017 Rowan Beentje (rowan.beent.je) & the Sequel Pro team. All rights reserved. + +SPMySQLFramework is free and open source software, licensed under [MIT](https://opensource.org/licenses/MIT). See [LICENSE](https://github.com/sequelpro/sequelpro/blob/master/Frameworks/SPMySQLFramework/LICENSE) for full details. + diff --git a/Frameworks/SPMySQLFramework/Readme.txt b/Frameworks/SPMySQLFramework/Readme.txt deleted file mode 100644 index 01e4c4b1..00000000 --- a/Frameworks/SPMySQLFramework/Readme.txt +++ /dev/null @@ -1,69 +0,0 @@ -The SPMySQL Framework is intended to provide a stable MySQL connection framework, with the ability -to run text-based queries and rapidly retrieve result sets with conversion from MySQL data types -to Cocoa objects. - -SPMySQL.framework has an interface loosely based around that provided by MCPKit by Serge Cohen and -Bertrand Mansion (http://mysql-cocoa.sourceforge.net/), and in particular the heavily modified -Sequel Pro version (http://www.sequelpro.com/). It is a full rewrite of the original framework, -although it includes code from patches implementing the following Sequel Pro functionality, largely -contributed by Hans-Jörg Bibiko, Stuart Connolly, Jakob Egger, and Rowan Beentje: - - - Connection locking (Jakob et al) - - Ping & keepalive (Rowan et al) - - Query cancellation (Rowan et al) - - Delegate setup (Stuart et al) - - SSL support (Rowan et al) - - Connection checking (Rowan et al) - - Version state (Stuart et al) - - Maximum packet size control (Hans et al) - - Result multithreading and streaming (Rowan et al) - - Improved encoding support & switching (Rowan et al) - - Database structure; moved to inside the app (Hans et al) - - Query reattempts and error-handling approach (Rowan et al) - - Geometry result class (Hans et al) - - Connection proxy (Stuart et al) - - -INTEGRATION - -SPMySQL.framework can be added to your project as a standard Cocoa framework, or the entire project -can be added as a subproject in Xcode. - -To add as a subproject in Xcode: - - 1) Add the SPMySQL framework's .xcodeproj to your current project - 2) Choose an existing target, Get Info, and under direct dependenies add a new dependency. Choose the SPMySQL.framework target from the sub-project - 3) Expand the subproject to see its child target - SPMySQL.framework. Drag this to the "Link Binary With Libraries" build phase of any targets using the framework. - 4) If you don't have a Copy Frameworks phase, add one; drag the SPMySQL.framework child target to this phase. - 5) In your build settings, add a User Header Search Path; make it a recursive path to the SPMySQL project folder location (for example ${PROJECT_DIR}/Frameworks/SPMySQLFramework). This should allow you to #include "SPMySQL.h" and have everything function. - -As a last resort jump onto IRC and join #sequel-pro on irc.freenode.net and any of the -developers will be more than happy to help you out. - - -LICENSE - -Copyright (c) 2012 Rowan Beentje (rowan.beent.je) and the Sequel Pro team. - -The SPMySQL framework is offered under the MIT license: - -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. diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h index 1e2a8c14..99daca77 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h +++ b/Frameworks/SPMySQLFramework/Source/SPMySQL Private APIs.h @@ -71,6 +71,7 @@ @interface SPMySQLConnection (Max_Packet_Size_Private_API) +- (NSInteger)_queryMaxAllowedPacketWithSQL:(NSString *)query resultInColumn:(NSUInteger)colIdx; - (void)_updateMaxQuerySize; - (void)_updateMaxQuerySizeEditability; - (BOOL)_attemptMaxQuerySizeIncreaseTo:(NSUInteger)targetSize; diff --git a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m index dc453624..76d1dfe7 100644 --- a/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m +++ b/Frameworks/SPMySQLFramework/Source/SPMySQLConnection Categories/Max Packet Size.m @@ -93,38 +93,57 @@ @implementation SPMySQLConnection (Max_Packet_Size_Private_API) /** - * Update the max_allowed_packet size - the largest supported query size - from the server. + * Executes a passed query for max_allowed_packet and returns the resulting number in bytes + * + * @return -1 => if the query failed + * 0 => if the query did not fail, but also did not contain a (valid) result + * * => if the query succeeded and the value is a valid integer (NOTE: this may also include -1 and 0) */ -- (void)_updateMaxQuerySize +- (NSInteger)_queryMaxAllowedPacketWithSQL:(NSString *)query resultInColumn:(NSUInteger)colIdx { - - // Determine which query to run based on server version - NSString *packetQueryString; - if ([self serverMajorVersion] == 3) { - packetQueryString = @"SHOW VARIABLES LIKE 'max_allowed_packet'"; - } else { - packetQueryString = @"SELECT @@global.max_allowed_packet"; - } - // Make a standard query to the server to retrieve the information - SPMySQLResult *result = [self queryString:packetQueryString]; - if(!result) { // query fails on sphinxql - NSLog(@"Query for max_allowed_packet failed: %@ (%lu) (on %@)", [self lastErrorMessage], [self lastErrorID], [self serverVersionString]); - return; + SPMySQLResult *result = [self queryString:query]; + if(!result) { + NSLog(@"Query (%@) for max_allowed_packet failed: %@ (%lu) (on %@)", query, [self lastErrorMessage], [self lastErrorID], [self serverVersionString]); + return -1; } [result setReturnDataAsStrings:YES]; // Get the maximum size string - NSString *maxQuerySizeString = nil; - if ([self serverMajorVersion] == 3) { - maxQuerySizeString = [[result getRowAsArray] objectAtIndex:1]; - } else { - maxQuerySizeString = [[result getRowAsArray] objectAtIndex:0]; - } + NSString *maxQuerySizeString = [[result getRowAsArray] objectAtIndex:colIdx]; + + NSInteger _maxQuerySize = maxQuerySizeString ? [maxQuerySizeString integerValue] : 0; - // If a valid size was returned, update the instance variable - if (maxQuerySizeString) { - maxQuerySize = (NSUInteger)[maxQuerySizeString integerValue]; + if(_maxQuerySize == 0) + NSLog(@"Query (%@) for max_allowed_packet returned invalid value: %ld (raw value: %@) (on %@)", query, _maxQuerySize, maxQuerySizeString, [self serverVersionString]); + + return _maxQuerySize; +} + +/** + * Update the max_allowed_packet size - the largest supported query size - from the server. + */ +- (void)_updateMaxQuerySize +{ + struct { + NSString *sql; + NSUInteger col; + } queryVariants[] = { + { .sql = @"SELECT @@global.max_allowed_packet", .col = 0 }, //works on mysql 4+ + { .sql = @"SHOW VARIABLES LIKE 'max_allowed_packet'", .col = 1 }, //works on mysql 3, sphinx + { .sql = nil, .col = 0 }, //terminator element + }; + + int i = 0; + while(queryVariants[i].sql) { + NSInteger _maxQuerySize = [self _queryMaxAllowedPacketWithSQL:queryVariants[i].sql resultInColumn:queryVariants[i].col]; + //see #2653 + if(_maxQuerySize >= 34) { // the max_allowed_packet query above has at least 34 bytes, so any value less than that would be nonsense + // If a valid size was returned, update the instance variable + maxQuerySize = (NSUInteger)_maxQuerySize; + return; + } + i++; } } diff --git a/Resources/Plists/PreferenceDefaults.plist b/Resources/Plists/PreferenceDefaults.plist index 1d55e65a..952b37c5 100644 --- a/Resources/Plists/PreferenceDefaults.plist +++ b/Resources/Plists/PreferenceDefaults.plist @@ -127,8 +127,6 @@ <false/> <key>FilterTableDefaultOperator</key> <string>LIKE '%@%'</string> - <key>GlobalResultTableFont</key> - <data>BAtzdHJlYW10eXBlZIHoA4QBQISEhAZOU0ZvbnQehIQITlNPYmplY3QAhYQBaSSEBVszNmNdBgAAABoAAAD//kwAdQBjAGkAZABhAEcAcgBhAG4AZABlAAAAhAFmC4QBYwCYAZgAmACG</data> <key>GrowlEnabled</key> <true/> <key>KeepAliveInterval</key> 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/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 26fdc1ab..161b5b0b 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; @@ -674,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 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/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 8c5c72ae..0298973d 100644 --- a/Source/SPFieldEditorController.m +++ b/Source/SPFieldEditorController.m @@ -37,6 +37,7 @@ #include <objc/objc-runtime.h> #import "SPCustomQuery.h" #import "SPTableContent.h" +#import "SPJSONFormatter.h" #import <SPMySQL/SPMySQL.h> @@ -195,6 +196,7 @@ typedef enum { } #endif + [self setEditedFieldInfo:nil]; if ( sheetEditData ) SPClear(sheetEditData); #ifndef SP_CODA if ( qlTypes ) SPClear(qlTypes); @@ -237,6 +239,7 @@ typedef enum { callerInstance = sender; _isGeometry = ([[fieldType uppercaseString] isEqualToString:@"GEOMETRY"]) ? YES : NO; + _isJSON = ([[fieldType uppercaseString] isEqualToString:SPMySQLJsonType]); // Set field label NSMutableString *label = [NSMutableString string]; @@ -249,7 +252,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 && !_isJSON) [label appendFormat:@"(%lld) ", maxTextLength]; if (!_allowNULL) @@ -352,7 +356,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"]); @@ -442,7 +447,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:@""]; @@ -651,6 +667,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]; @@ -1358,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; 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 <https://github.com/sequelpro/sequelpro> + +#import <Foundation/Foundation.h> + +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..258845c9 --- /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 <https://github.com/sequelpro/sequelpro> + +#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(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 && prevTokenType != JSON_TOK_COMMA && (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 = [input lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + 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); +} diff --git a/Source/SPSQLExporter.m b/Source/SPSQLExporter.m index 8d53d0b3..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, k, 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:@""]; @@ -369,14 +366,17 @@ [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; + 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]) { @@ -392,11 +392,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 +407,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,19 +415,19 @@ [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 { [sqlString setString:@",\n\t("]; } - for (t = 0; t < colCount; t++) + for (NSUInteger t = 0; t < colCount; t++) { id object = NSArrayObjectAtIndex(row, t); @@ -506,6 +503,9 @@ sqlExportPool = [[NSAutoreleasePool alloc] init]; cleanAutoReleasePool = NO; } + + rowsWrittenForTable++; + rowsWrittenForCurrentStmt++; } // Complete the command 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 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 - diff --git a/UnitTests/SPJSONFormatterTests.m b/UnitTests/SPJSONFormatterTests.m new file mode 100644 index 00000000..658169a4 --- /dev/null +++ b/UnitTests/SPJSONFormatterTests.m @@ -0,0 +1,141 @@ +// +// SPJSONFormatterTests.m +// sequel-pro +// +// Created by Max Lohrmann on 12.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 <https://github.com/sequelpro/sequelpro> + +#import "SPJSONFormatter.h" +#import <Cocoa/Cocoa.h> +#import <XCTest/XCTest.h> + +@interface SPJSONFormatterTests : XCTestCase + +- (void)testFormatting; +- (void)testUnformatting; + +@end + +@implementation SPJSONFormatterTests + +- (void)testFormatting +{ + + //invalid input + XCTAssertNil([SPJSONFormatter stringByFormattingString:nil],@"nil output on nil input"); + + //empty string + XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:@""], @"", @"empty string stays empty"); + + //scalars on their own should not get changed + { + NSArray *scalars = @[@"true",@"false",@"null",@"123.45",@"1.4e-5",@"\"string\""]; + for (NSString *scalar in scalars) { + XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:scalar], scalar, @"scalar only input stays as is"); + } + } + + //simple test involving all types + { + NSString *unf = @"{\"key\": null, \"foo\": [true, false], \"ba\\\"r\": [{},{\"key2\": -1.98}]}"; + NSString *fmt = @"{\n\t\"key\": null,\n\t\"foo\": [\n\t\ttrue,\n\t\tfalse\n\t],\n\t\"ba\\\"r\": [\n\t\t{},\n\t\t{\n\t\t\t\"key2\": -1.98\n\t\t}\n\t]\n}"; + + XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:unf], fmt, @"simple formatting test"); + } + + //other tests + { + NSArray *tests = @[ + @[@"{\"key\": \"v\0al\"}",@"{\n\t\"key\": \"v\0al\"\n}", @"NUL in input (invalid JSON)"], + @[@"[\"\",\"\"\",\"", @"[\n\t\"\",\n\t\"\"\",\"", @"series of dquotes (invalid JSON)"], + @[@"{[{\"ab\\u0090c\",",@"{\n\t[\n\t\t{\n\t\t\t\"ab\\u0090c\",\n",@"unterminated elements (invalid JSON)"], + @[@"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[null]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]",@"[\n\t[\n\t\t[\n\t\t\t[\n\t\t\t\t[\n\t\t\t\t\t[\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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\t[\n\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\t\tnull\n\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\t]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t]\n\t\t]\n\t]\n]",@"34 levels of indent"], + @[@"{\"a\":\"bcd}",@"{\n\t\"a\": \"bcd}",@"unterminated string (invalid JSON)"], + @[@"[1,\"ab\ncd\",3]",@"[\n\t1,\n\t\"ab\ncd\",\n\t3\n]",@"multiline string (invalid JSON)"], + @[@"{}}},false]",@"{}\n}\n},\nfalse\n]",@"closing something that is not open (invalid JSON)"], + @[@"[[123e4}}",@"[\n\t[\n\t\t123e4\n\t}\n}",@"unmatched braces (invalid JSON)"], + @[@"[{]}",@"[\n\t{\n\t]\n}",@"unmatched braces 2 (invalid JSON)"], + @[@"[ true , \n false \t ] \t \n",@"[\n\ttrue,\n\tfalse\n]",@"whitespace reformatting"], + @[@"[1,2,]",@"[\n\t1,\n\t2,\n]",@"trailing comma (valid for some parsers)"], + @[@"{}/{|}],-\"}[|[{}\\\"]:{~]\",,}{]\{|::[\\|\"],};]*}]",@"{}/{\n\t|\n}\n],\n-\"}[|[{}\\\"]:{~]\",\n,\n}{\n]{\n\t|: : [\n\t\t\\|\"],};]*}]",@"random garbage"], + ]; + + for (NSArray *pair in tests) { + XCTAssertEqualObjects([SPJSONFormatter stringByFormattingString:[pair objectAtIndex:0]], [pair objectAtIndex:1], @"%@", [pair objectAtIndex:2]); + } + } +} + +- (void)testUnformatting +{ + //invalid input + XCTAssertNil([SPJSONFormatter stringByUnformattingString:nil],@"nil output on nil input"); + + //empty string + XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:@""], @"", @"empty string stays empty"); + + //scalars on their own should not get changed + { + NSArray *scalars = @[@"true",@"false",@"null",@"123.45",@"1.4e-5",@"\"string\""]; + for (NSString *scalar in scalars) { + XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:scalar], scalar, @"scalar only input stays as is"); + } + } + + //simple test involving all types + { + NSString *unf = @"{\"key\": null, \"foo\": [true, false], \"ba\\\"r\": [{}, {\"key2\": -1.98}]}"; + NSString *fmt = @"{\n\t\"key\": null,\n\t\"foo\": [\n\t\ttrue,\n\t\tfalse\n\t],\n\t\"ba\\\"r\": [\n\t\t{},\n\t\t{\n\t\t\t\"key2\": -1.98\n\t\t}\n\t]\n}"; + + XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:fmt], unf, @"simple unformatting test"); + } + + //other tests + { + NSArray *tests = @[ + @[@"{\n\t\"key\": \"v\0al\"\n}", @"{\"key\": \"v\0al\"}", @"NUL in input (invalid JSON)"], + @[@"[\n\t\"\",\n\t\"\"\",\"", @"[\"\", \"\"\",\"", @"series of dquotes (invalid JSON)"], + @[@"{\n\t[\n\t\t{\n\t\t\t\"ab\\u0090c\",\n",@"{[{\"ab\\u0090c\", ",@"unterminated elements (invalid JSON)"], + @[@"[\n\t[\n\t\t[\n\t\t\t[\n\t\t\t\t[\n\t\t\t\t\t[\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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[\n\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\t[\n\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\t\tnull\n\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\t]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\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]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t]\n\t\t\t\t\t]\n\t\t\t\t]\n\t\t\t]\n\t\t]\n\t]\n]", @"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[null]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]",@"34 levels of indent"], + @[@"{\n\t\"a\": \"bcd}", @"{\"a\": \"bcd}",@"unterminated string (invalid JSON)"], + @[@"[\n\t1,\n\t\"ab\ncd\",\n\t3\n]",@"[1, \"ab\ncd\", 3]",@"multiline string (invalid JSON)"], + @[@"{}\n}\n},\nfalse\n]", @"{}}}, false]",@"closing something that is not open (invalid JSON)"], + @[@"[\n\t[\n\t\t123e4\n\t}\n}", @"[[123e4}}",@"unmatched braces (invalid JSON)"], + @[@"[\n\t{\n\t]\n}", @"[{]}",@"unmatched braces 2 (invalid JSON)"], + @[@"[ true , \n false \t ] \t \n",@"[true, false]",@"whitespace reformatting"], + @[@"[\n\t1,\n\t2,\n]", @"[1, 2, ]",@"trailing comma (valid for some parsers)"], + @[@"{}/{\n\t|\n}\n],\n-\"}[|[{}\\\"]:{~]\",\n,\n}{\n]{\n\t|: : [\n\t\t\\|\"],};]*}]", @"{}/{|}], -\"}[|[{}\\\"]:{~]\", , }{]\{|: : [\\|\"],};]*}]",@"random garbage"], + ]; + + for (NSArray *pair in tests) { + XCTAssertEqualObjects([SPJSONFormatter stringByUnformattingString:[pair objectAtIndex:0]], [pair objectAtIndex:1], @"%@", [pair objectAtIndex:2]); + } + } + + +} + +@end @@ -1,11 +1,11 @@ -Sequel Pro +Sequel Pro <img alt="Logo" src="https://sequelpro.com/images/logo.png" align="right" height="50"> ========== Sequel Pro is a fast, easy-to-use Mac database management application for working with MySQL databases. You can find more details on our website: [sequelpro.com](http://sequelpro.com) -![Screenshot](http://www.sequelpro.com/assets/images/NewAdvancedFilter.jpg) +![Screenshot](https://sequelpro.com/images/browse.png) Build Instructions ================== diff --git a/sequel-pro.xcodeproj/project.pbxproj b/sequel-pro.xcodeproj/project.pbxproj index 272944ff..b84b20e4 100644 --- a/sequel-pro.xcodeproj/project.pbxproj +++ b/sequel-pro.xcodeproj/project.pbxproj @@ -224,6 +224,8 @@ 5080229C1BF7C0FE0052A9B2 /* MGTemplateStandardMarkers.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8AD0F909194002A3258 /* MGTemplateStandardMarkers.m */; }; 5080229D1BF7C0FE0052A9B2 /* MGTemplateStandardFilters.m in Sources */ = {isa = PBXBuildFile; fileRef = 296DC8B40F909194002A3258 /* MGTemplateStandardFilters.m */; }; 50805B0D1BF2A068005F7A99 /* SPPopUpButtonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 50805B0C1BF2A068005F7A99 /* SPPopUpButtonCell.m */; }; + 50837F741E50DCD4004FAE8A /* SPJSONFormatterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */; }; + 50837F771E50E007004FAE8A /* SPJSONFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 73F70A951E4E547500636550 /* SPJSONFormatter.m */; }; 5089B0271BE714E300E226CD /* SPIdMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 5089B0261BE714E300E226CD /* SPIdMenu.m */; }; 50A9F8B119EAD4B90053E571 /* SPGotoDatabaseController.m in Sources */ = {isa = PBXBuildFile; fileRef = 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */; }; 50D3C3491A75B8A800B5429C /* GotoDatabaseDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 50D3C34B1A75B8A800B5429C /* GotoDatabaseDialog.xib */; }; @@ -415,6 +417,7 @@ 58FEEF471676D160009CD478 /* SQL.icns in Resources */ = {isa = PBXBuildFile; fileRef = 58FEEF441676D160009CD478 /* SQL.icns */; }; 58FEF16D0F23D66600518E8E /* SPSQLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF16C0F23D66600518E8E /* SPSQLParser.m */; }; 58FEF57E0F3B4E9700518E8E /* SPTableData.m in Sources */ = {isa = PBXBuildFile; fileRef = 58FEF57D0F3B4E9700518E8E /* SPTableData.m */; }; + 73F70A961E4E547500636550 /* SPJSONFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 73F70A951E4E547500636550 /* SPJSONFormatter.m */; }; 8D15AC340486D014006FF6A4 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */; }; B51D6B9E114C310C0074704E /* toolbar-switch-to-table-triggers.png in Resources */ = {isa = PBXBuildFile; fileRef = B51D6B9D114C310C0074704E /* toolbar-switch-to-table-triggers.png */; }; B52460D70F8EF92300171639 /* SPArrayAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = B52460D40F8EF92300171639 /* SPArrayAdditions.m */; }; @@ -966,6 +969,7 @@ 508022941BF7BA470052A9B2 /* English */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = English; path = English.lproj/SPQLPluginExportSettingsTemplate.html; sourceTree = "<group>"; }; 50805B0B1BF2A068005F7A99 /* SPPopUpButtonCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPPopUpButtonCell.h; sourceTree = "<group>"; }; 50805B0C1BF2A068005F7A99 /* SPPopUpButtonCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPPopUpButtonCell.m; sourceTree = "<group>"; }; + 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPJSONFormatterTests.m; sourceTree = "<group>"; }; 5089B0251BE714E300E226CD /* SPIdMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPIdMenu.h; sourceTree = "<group>"; }; 5089B0261BE714E300E226CD /* SPIdMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPIdMenu.m; sourceTree = "<group>"; }; 50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPGotoDatabaseController.h; sourceTree = "<group>"; }; @@ -1217,6 +1221,8 @@ 58FEF16C0F23D66600518E8E /* SPSQLParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPSQLParser.m; sourceTree = "<group>"; }; 58FEF57C0F3B4E9700518E8E /* SPTableData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPTableData.h; sourceTree = "<group>"; }; 58FEF57D0F3B4E9700518E8E /* SPTableData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPTableData.m; sourceTree = "<group>"; }; + 73F70A941E4E547500636550 /* SPJSONFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPJSONFormatter.h; sourceTree = "<group>"; }; + 73F70A951E4E547500636550 /* SPJSONFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPJSONFormatter.m; sourceTree = "<group>"; }; 8D15AC370486D014006FF6A4 /* Sequel Pro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sequel Pro.app"; sourceTree = BUILT_PRODUCTS_DIR; }; B51D6B9D114C310C0074704E /* toolbar-switch-to-table-triggers.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "toolbar-switch-to-table-triggers.png"; sourceTree = "<group>"; }; B52460D30F8EF92300171639 /* SPArrayAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPArrayAdditions.h; sourceTree = "<group>"; }; @@ -1671,13 +1677,13 @@ BC27779F11514B940034DF6A /* SPNavigatorController.m */, 17A7773211C52D8E001E27B4 /* SPIndexesController.h */, 17A7773311C52D8E001E27B4 /* SPIndexesController.m */, - 17846B9D170C95D800414499 /* Process List */, - 17381853151FB29C0078FFE2 /* User Manager */, - 1713C73D140D88D400CFD461 /* Query Controller */, 50A9F8AF19EAD4B90053E571 /* SPGotoDatabaseController.h */, 50A9F8B019EAD4B90053E571 /* SPGotoDatabaseController.m */, 506CE92F1A311C6C0039F736 /* SPTableContentFilterController.h */, 506CE9301A311C6C0039F736 /* SPTableContentFilterController.m */, + 1713C73D140D88D400CFD461 /* Query Controller */, + 17381853151FB29C0078FFE2 /* User Manager */, + 17846B9D170C95D800414499 /* Process List */, ); name = "Subview Controllers"; sourceTree = "<group>"; @@ -2469,6 +2475,7 @@ children = ( 50D3C35B1A771C4C00B5429C /* SPParserUtilsTest.m */, 503B02CE1AE95C2C0060CAB1 /* SPTableFilterParserTest.m */, + 50837F731E50DCD4004FAE8A /* SPJSONFormatterTests.m */, ); name = Other; sourceTree = "<group>"; @@ -2664,6 +2671,8 @@ 50D3C3511A77135F00B5429C /* SPParserUtils.h */, 503B02C81AE82C5E0060CAB1 /* SPTableFilterParser.h */, 503B02C91AE82C5E0060CAB1 /* SPTableFilterParser.m */, + 73F70A941E4E547500636550 /* SPJSONFormatter.h */, + 73F70A951E4E547500636550 /* SPJSONFormatter.m */, ); name = Parsing; sourceTree = "<group>"; @@ -3175,6 +3184,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 50837F771E50E007004FAE8A /* SPJSONFormatter.m in Sources */, 505F56901BCEE491007467DD /* SPOSInfo.m in Sources */, 505F568F1BCEE485007467DD /* SPFunctions.m in Sources */, 502D21F81BA50966000D4CE7 /* SPDataAdditions.m in Sources */, @@ -3184,6 +3194,7 @@ 503B02CF1AE95C2C0060CAB1 /* SPTableFilterParserTest.m in Sources */, 50EA92681AB23EFC008D3C4F /* SPTableCopy.m in Sources */, 507FF1621BBF0D5000104523 /* SPTableCopyTest.m in Sources */, + 50837F741E50DCD4004FAE8A /* SPJSONFormatterTests.m in Sources */, 50EA926A1AB246B8008D3C4F /* SPDatabaseActionTest.m in Sources */, 50EA92651AB23EC8008D3C4F /* SPDatabaseAction.m in Sources */, 50EA92641AB23EAD008D3C4F /* SPDatabaseCopy.m in Sources */, @@ -3311,6 +3322,7 @@ 1740FABB0FC4372F00CF3699 /* SPDatabaseData.m in Sources */, 17C058880FC9FC390077E9CF /* SPNarrowDownCompletion.m in Sources */, 177E7A230FCB6A2E00E9E122 /* SPExtendedTableInfo.m in Sources */, + 73F70A961E4E547500636550 /* SPJSONFormatter.m in Sources */, 58CDB3300FCE138D00F8ACA3 /* SPSSHTunnel.m in Sources */, 29A1B7E50FD1293A000B88E8 /* SPPrintAccessory.m in Sources */, BC1847EA0FE6EC8400094BFB /* SPEditSheetTextView.m in Sources */, |