Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote-tracking branch 'origin/master'

Conflicts:
	Source/ChangeTracker/TDChangeTracker.m
	Source/ChangeTracker/TDSocketChangeTracker.m
	Source/TDBody.m
	Source/TDCollateJSON.m
	Source/TDDatabase+Attachments.h
	Source/TDDatabase+Insertion.m
	Source/TDDatabase+LocalDocs.m
	Source/TDDatabase.m
	Source/TDJSON.h
	Source/TDJSON.m
	Source/TDMisc.m
	Source/TDMultipartDownloader.m
	Source/TDPuller.m
	Source/TDPusher.m
	Source/TDRemoteRequest.m
	Source/TDReplicatorManager.h
	Source/TDReplicator_Tests.m
	Source/TDRouter+Handlers.m
	Source/TDRouter.h
	Source/TDRouter.m
	Source/TDRouter_Tests.m
	Source/TDServer.m
	Source/TDURLProtocol.m
	Source/TDView.m
	TouchDB.xcodeproj/project.pbxproj
  • Loading branch information...
commit 487f6f34108e0987e932982f72bcb465efd8b93a 1 parent 2a463d3
@snej snej authored
Showing with 4,107 additions and 1,624 deletions.
  1. +7 −4 Demo-Mac/DemoAppController.m
  2. +8 −1 Demo-Mac/TouchServ.m
  3. +6 −0 GNUmakefile
  4. +4 −0 Listener/TDHTTPResponse.h
  5. +94 −70 Listener/TDHTTPResponse.m
  6. +3 −4 Listener/TDListener.h
  7. +3 −7 Listener/TDListener.m
  8. +4 −4 README.md
  9. +3 −0  Source/ChangeTracker/TDChangeTracker.h
  10. +22 −15 Source/ChangeTracker/TDChangeTracker.m
  11. +5 −4 Source/ChangeTracker/TDConnectionChangeTracker.m
  12. +4 −0 Source/ChangeTracker/TDSocketChangeTracker.h
  13. +186 −65 Source/ChangeTracker/TDSocketChangeTracker.m
  14. +36 −0 Source/TDAttachment.h
  15. +58 −0 Source/TDAttachment.m
  16. +3 −2 Source/TDBlobStore.h
  17. +12 −1 Source/TDBlobStore.m
  18. +2 −2 Source/TDBody.m
  19. +44 −0 Source/TDCanonicalJSON.h
  20. +288 −0 Source/TDCanonicalJSON.m
  21. +4 −5 Source/TDCollateJSON.m
  22. +16 −5 Source/TDDatabase+Attachments.h
  23. +231 −133 Source/TDDatabase+Attachments.m
  24. +1 −1  Source/TDDatabase+Insertion.h
  25. +144 −67 Source/TDDatabase+Insertion.m
  26. +14 −14 Source/TDDatabase+LocalDocs.m
  27. +4 −5 Source/TDDatabase+Replication.m
  28. +7 −6 Source/TDDatabase.h
  29. +45 −161 Source/TDDatabase.m
  30. +38 −0 Source/TDDatabaseManager.h
  31. +215 −0 Source/TDDatabaseManager.m
  32. +158 −66 Source/TDDatabase_Tests.m
  33. +14 −0 Source/TDGNUstep.h
  34. +7 −1 Source/TDGNUstep.m
  35. +28 −14 Source/TDInternal.h
  36. +19 −5 Source/TDJSON.h
  37. +70 −5 Source/TDJSON.m
  38. +10 −1 Source/TDMisc.h
  39. +67 −16 Source/TDMisc.m
  40. +44 −0 Source/TDMultipartDocumentReader.h
  41. +282 −0 Source/TDMultipartDocumentReader.m
  42. +4 −12 Source/TDMultipartDownloader.h
  43. +28 −157 Source/TDMultipartDownloader.m
  44. +1 −0  Source/TDMultipartUploader.h
  45. +4 −1 Source/TDMultipartUploader.m
  46. +4 −0 Source/TDPuller.h
  47. +195 −65 Source/TDPuller.m
  48. +33 −28 Source/TDPusher.m
  49. +7 −0 Source/TDReachability.m
  50. +6 −0 Source/TDRemoteRequest.h
  51. +28 −5 Source/TDRemoteRequest.m
  52. +28 −0 Source/TDReplicator.h
  53. +91 −31 Source/TDReplicator.m
  54. +4 −3 Source/TDReplicatorManager.h
  55. +45 −28 Source/TDReplicatorManager.m
  56. +36 −19 Source/TDReplicator_Tests.m
  57. +9 −0 Source/TDRevision.h
  58. +125 −2 Source/TDRevision.m
  59. +167 −153 Source/TDRouter+Handlers.m
  60. +20 −7 Source/TDRouter.h
  61. +156 −46 Source/TDRouter.m
  62. +203 −102 Source/TDRouter_Tests.m
  63. +10 −14 Source/TDServer.h
  64. +69 −122 Source/TDServer.m
  65. +49 −0 Source/TDStatus.h
  66. +69 −0 Source/TDStatus.m
  67. +18 −0 Source/TDURLProtocol.h
  68. +168 −41 Source/TDURLProtocol.m
  69. +102 −48 Source/TDView.m
  70. +135 −20 Source/TDView_Tests.m
  71. +76 −34 TouchDB.xcodeproj/project.pbxproj
  72. +1 −0  TouchDB.xcodeproj/xcshareddata/xcschemes/Mac Demo.xcscheme
  73. +4 −0 TouchDB.xcodeproj/xcshareddata/xcschemes/iOS Demo.xcscheme
  74. +1 −1  vendor/MYUtilities
  75. +1 −1  vendor/fmdb
View
11 Demo-Mac/DemoAppController.m
@@ -102,11 +102,14 @@ - (void) applicationDidFinishLaunching: (NSNotification*)n {
#ifdef FOR_TESTING_PURPOSES
// Start a listener socket:
- sListener = [[TDListener alloc] initWithTDServer: server.touchServer port: 8888];
- [sListener start];
+ [server tellTDServer: ^(TDServer* tdServer) {
+ // Register support for handling certain JS functions used in the CouchDB unit tests:
+ [TDView setCompiler: self];
+
+ sListener = [[TDListener alloc] initWithTDServer: tdServer port: 8888];
+ [sListener start];
+ }];
- // Register support for handling certain JS functions used in the CouchDB unit tests:
- [TDView setCompiler: self];
#endif
}
View
9 Demo-Mac/TouchServ.m
@@ -60,9 +60,16 @@ int main (int argc, const char * argv[])
// Start a listener socket:
TDListener* listener = [[TDListener alloc] initWithTDServer: server port: kPortNumber];
+
+ if (argc >= 2 && strcmp(argv[1], "--readonly") == 0)
+ listener.readOnly = YES;
+
[listener start];
- Log(@"TouchServ %@ is listening on port %d ... relax!", [TDRouter versionString], kPortNumber);
+ Log(@"TouchServ %@ is listening%@ on port %d ... relax!",
+ [TDRouter versionString],
+ (listener.readOnly ? @" in read-only mode" : @""),
+ kPortNumber);
[[NSRunLoop currentRunLoop] run];
View
6 GNUmakefile
@@ -13,9 +13,11 @@ TouchDB_OBJC_FILES = \
Source/TDDatabase+Attachments.m \
Source/TDDatabase+Insertion.m \
Source/TDDatabase+LocalDocs.m \
+ Source/TDAttachment.m \
Source/TDBody.m \
Source/TDRevision.m \
Source/TDView.m \
+ Source/TDDatabaseManager.m \
Source/TDServer.m \
Source/TDBlobStore.m \
\
@@ -29,6 +31,7 @@ TouchDB_OBJC_FILES = \
Source/TDPusher.m \
Source/TDReplicatorManager.m \
Source/TDRemoteRequest.m \
+ Source/TDMultipartDocumentReader.m \
Source/TDMultipartDownloader.m \
Source/TDMultipartReader.m \
Source/TDMultipartUploader.m \
@@ -37,12 +40,14 @@ TouchDB_OBJC_FILES = \
Source/TDReachability_Stubs.m \
\
Source/TDBatcher.m \
+ Source/TDCanonicalJSON.m \
Source/TDCollateJSON.m \
Source/TDGNUstep.m \
Source/TDBase64.m \
Source/TDJSON.m \
Source/TDMisc.m \
Source/TDSequenceMap.m \
+ Source/TDStatus.m \
\
Source/TDBlobStore_Tests.m \
Source/TDDatabase_Tests.m \
@@ -86,6 +91,7 @@ TouchDB_HEADER_FILES = \
TDRevision.h \
TDRouter.h \
TDServer.h \
+ TDStatus.h \
TDURLProtocol.h \
TDView.h \
TDC.h
View
4 Listener/TDHTTPResponse.h
@@ -16,7 +16,9 @@
TDHTTPConnection* _connection;
TDResponse* _response;
BOOL _finished;
+ BOOL _askedIfChunked;
BOOL _chunked;
+ BOOL _delayedHeaders;
NSMutableData* _data; // Data received, waiting to be read by the connection
UInt64 _dataOffset; // Offset in response of 1st byte of _data
UInt64 _offset; // Offset in response for next readData
@@ -24,4 +26,6 @@
- (id) initWithRouter: (TDRouter*)router forConnection:(TDHTTPConnection*)connection;
+@property UInt64 offset;
+
@end
View
164 Listener/TDHTTPResponse.m
@@ -24,7 +24,7 @@
@interface TDHTTPResponse ()
- (void) onResponseReady: (TDResponse*)response;
-- (void) onDataAvailable: (NSData*)data;
+- (void) onDataAvailable: (NSData*)data finished: (BOOL)finished;
- (void) onFinished;
@end
@@ -41,17 +41,26 @@ - (id) initWithRouter: (TDRouter*)router forConnection:(TDHTTPConnection*)connec
router.onResponseReady = ^(TDResponse* r) {
[self onResponseReady: r];
};
- router.onDataAvailable = ^(NSData* data) {
- [self onDataAvailable: data];
+ router.onDataAvailable = ^(NSData* data, BOOL finished) {
+ [self onDataAvailable: data finished: finished];
};
router.onFinished = ^{
[self onFinished];
};
+
+ if (connection.listener.readOnly) {
+ router.onAccessCheck = ^TDStatus(TDDatabase* db, NSString* docID, SEL action) {
+ NSString* method = router.request.HTTPMethod;
+ if (![method isEqualToString: @"GET"] && ![method isEqualToString: @"HEAD"])
+ return kTDStatusForbidden;
+ return kTDStatusOK;
+ };
+ }
// Run the router, synchronously:
LogTo(TDListenerVerbose, @"%@: Starting...", self);
- [_connection.listener onServerThread: ^{[router start];}];
- _chunked = !_finished;
+ [router start];
+ LogTo(TDListenerVerbose, @"%@: Returning from -init", self);
}
return self;
}
@@ -75,109 +84,123 @@ - (NSString*) description {
* implement this method in your custom response class and return YES.
**/
- (BOOL) isChunked {
- return _chunked;
+ @synchronized(self) {
+ if (!_askedIfChunked) {
+ _chunked = !_finished;
+ }
+ LogTo(TDListenerVerbose, @"%@ answers isChunked=%d", self, _chunked);
+ return _chunked;
+ }
}
-- (BOOL) delayResponeHeaders {
- return _chunked && !_response;
+- (BOOL) delayResponeHeaders { // [sic]
+ @synchronized(self) {
+ LogTo(TDListenerVerbose, @"%@ answers delayResponeHeaders=%d", self, !_response);
+ if (!_response)
+ _delayedHeaders = YES;
+ return !_response;
+ }
}
- (void) onResponseReady: (TDResponse*)response {
- _response = [response retain];
- LogTo(TDListener, @" %@ --> %i", self, _response.status);
- if (_chunked)
- [_connection responseHasAvailableData: self];
+ @synchronized(self) {
+ _response = [response retain];
+ LogTo(TDListener, @" %@ --> %i", self, _response.status);
+ if (_delayedHeaders)
+ [_connection responseHasAvailableData: self];
+ }
}
- (NSInteger) status {
+ LogTo(TDListenerVerbose, @"%@ answers status=%d", self, _response.status);
return _response.status;
}
- (NSDictionary *) httpHeaders {
+ LogTo(TDListenerVerbose, @"%@ answers httpHeaders={%d headers}", self, _response.headers.count);
return _response.headers;
}
-- (void) onDataAvailable: (NSData*)data {
- LogTo(TDListenerVerbose, @"%@ adding %u bytes", self, (unsigned)data.length);
- if (_data)
- [_data appendData: data];
- else
- _data = [data mutableCopy];
- if (_chunked)
- [_connection responseHasAvailableData: self];
+- (void) onDataAvailable: (NSData*)data finished: (BOOL)finished {
+ @synchronized(self) {
+ LogTo(TDListenerVerbose, @"%@ adding %u bytes", self, (unsigned)data.length);
+ if (_data)
+ [_data appendData: data];
+ else
+ _data = [data mutableCopy];
+ if (finished)
+ [self onFinished];
+ else if (_chunked)
+ [_connection responseHasAvailableData: self];
+ }
}
-- (UInt64) offset {return _offset;}
-- (void) setOffset: (UInt64)offset {_offset = offset;}
+@synthesize offset=_offset;
+
- (UInt64) contentLength {
- if (!_finished)
- return 0;
- return _dataOffset + _data.length;
+ @synchronized(self) {
+ if (!_finished)
+ return 0;
+ return _dataOffset + _data.length;
+ }
}
- (NSData*) readDataOfLength: (NSUInteger)length {
- NSAssert(_offset >= _dataOffset, @"Invalid offset %llu, min is %llu", _offset, _dataOffset);
- NSRange range;
- range.location = (NSUInteger)(_offset - _dataOffset);
- if (range.location >= _data.length)
- return nil;
- NSUInteger bytesAvailable = _data.length - range.location;
- range.length = MIN(length, bytesAvailable);
- NSData* result = [_data subdataWithRange: range];
- _offset += range.length;
- if (range.length == bytesAvailable) {
- // Client has read all of the available data, so we can discard it
- _dataOffset += _data.length;
- [_data autorelease];
- _data = nil;
+ @synchronized(self) {
+ NSAssert(_offset >= _dataOffset, @"Invalid offset %llu, min is %llu", _offset, _dataOffset);
+ NSRange range;
+ range.location = (NSUInteger)(_offset - _dataOffset);
+ if (range.location >= _data.length) {
+ LogTo(TDListenerVerbose, @"%@ sending nil bytes", self);
+ return nil;
+ }
+ NSUInteger bytesAvailable = _data.length - range.location;
+ range.length = MIN(length, bytesAvailable);
+ NSData* result = [_data subdataWithRange: range];
+ _offset += range.length;
+ if (range.length == bytesAvailable) {
+ // Client has read all of the available data, so we can discard it
+ _dataOffset += _data.length;
+ [_data autorelease];
+ _data = nil;
+ }
+ LogTo(TDListenerVerbose, @"%@ sending %u bytes", self, result.length);
+ return result;
}
- LogTo(TDListenerVerbose, @"%@ sending %u bytes", self, result.length);
- return result;
}
- (BOOL) isDone {
+ LogTo(TDListenerVerbose, @"%@ answers isDone=%d", self, _finished);
return _finished;
}
- (void) onFinished {
- if (_finished)
- return;
- _finished = true;
-
- LogTo(TDListenerVerbose, @"%@ Finished!", self);
-
- // Break cycles:
- _router.onResponseReady = nil;
- _router.onDataAvailable = nil;
- _router.onFinished = nil;
-
- if (!_chunked) {
- // Response finished immediately, before the connection asked for any data, so we're free
- // to massage the response:
- int status = _response.status;
- if (status >= 300 && _data.length == 0) {
- // Put a generic error message in the body:
- NSString* errorMsg;
- switch (status) {
- case 404: errorMsg = @"not_found"; break;
- // TODO: There are more of these to add; see error_info() in couch_httpd.erl
- default:
- errorMsg = [NSHTTPURLResponse localizedStringForStatusCode: status];
- }
- NSString* responseStr = [NSString stringWithFormat: @"{\"status\": %i, \"error\":\"%@\"}\n",
- status, errorMsg];
- [self onDataAvailable: [responseStr dataUsingEncoding: NSUTF8StringEncoding]];
- [_response.headers setObject: @"text/plain; encoding=UTF-8" forKey: @"Content-Type"];
- } else {
+ @synchronized(self) {
+ if (_finished)
+ return;
+ _finished = true;
+ _askedIfChunked = true;
+
+ LogTo(TDListenerVerbose, @"%@ Finished!", self);
+
+ // Break cycles:
+ _router.onResponseReady = nil;
+ _router.onDataAvailable = nil;
+ _router.onFinished = nil;
+
+ if (!_chunked || _offset == 0) {
+ // Response finished immediately, before the connection asked for any data, so we're free
+ // to massage the response:
+ LogTo(TDListenerVerbose, @"%@ prettifying response body", self);
#if DEBUG
BOOL pretty = YES;
#else
@@ -188,6 +211,7 @@ - (void) onFinished {
_data = [_response.body.asPrettyJSON mutableCopy];
}
}
+ [_connection responseHasAvailableData: self];
}
}
View
7 Listener/TDListener.h
@@ -15,15 +15,14 @@
{
TDHTTPServer* _httpServer;
TDServer* _tdServer;
- dispatch_queue_t _queue;
+ BOOL _readOnly;
}
- (id) initWithTDServer: (TDServer*)server port: (UInt16)port;
+@property BOOL readOnly;
+
- (BOOL) start;
- (void) stop;
-/** Runs the block *synchronously* on the single server thread. */
-- (void) onServerThread: (void(^)())block;
-
@end
View
10 Listener/TDListener.m
@@ -24,6 +24,9 @@
@implementation TDListener
+@synthesize readOnly=_readOnly;
+
+
- (id) initWithTDServer: (TDServer*)server port: (UInt16)port {
self = [super init];
if (self) {
@@ -33,7 +36,6 @@ - (id) initWithTDServer: (TDServer*)server port: (UInt16)port {
_httpServer.tdServer = _tdServer;
_httpServer.port = port;
_httpServer.connectionClass = [TDHTTPConnection class];
- _queue = dispatch_queue_create("TDListener", DISPATCH_QUEUE_SERIAL);
}
return self;
}
@@ -44,16 +46,10 @@ - (void)dealloc
[self stop];
[_tdServer release];
[_httpServer release];
- dispatch_release(_queue);
[super dealloc];
}
-- (void) onServerThread: (void(^)())block {
- dispatch_sync(_queue, block);
-}
-
-
- (BOOL) start {
NSError* error;
return [_httpServer start: &error];
View
8 README.md
@@ -1,8 +1,8 @@
# TouchDB #
by Jens Alfke (jens@couchbase.com)
-with contributions from Alexander Edge, Chris Kau, Marty Schoch, Paul Mietz Egli
-and technical advice from Damien Katz, Filipe Manana, J Chris Anderson
+with contributions from Alexander Edge, Chris Kau, J Chris Anderson, Marty Schoch, Paul Mietz Egli
+and technical advice from Damien Katz and Filipe Manana
**TouchDB** is a lightweight [CouchDB][1]-compatible database engine suitable for embedding into mobile or desktop apps. Think of it this way: If CouchDB is MySQL, then TouchDB is SQLite.
@@ -22,7 +22,7 @@ More documentation is available on the [wiki][2].
## Requirements ##
* It's written in Objective-C.
- * Xcode 4.2+ is required to build it.
+ * Xcode 4.3+ is required to build it.
* Runtime system requirements are iOS 5+, or Mac OS X 10.7.2+.
Looking for the [Java/Android implementation][11]? It has its own repository.
@@ -39,7 +39,7 @@ Looking for the [Java/Android implementation][11]? It has its own repository.
TouchDB recently went alpha (at the end of January 2012; version 0.45.)
-Beta should arrive before spring.
+Beta should arrive by summer.
## Building TouchDB ##
View
3  Source/ChangeTracker/TDChangeTracker.h
@@ -41,12 +41,14 @@ typedef enum TDChangeTrackerMode {
TDChangeTrackerMode _mode;
id _lastSequenceID;
NSError* _error;
+ BOOL _includeConflicts;
NSString* _filterName;
NSDictionary* _filterParameters;
}
- (id)initWithDatabaseURL: (NSURL*)databaseURL
mode: (TDChangeTrackerMode)mode
+ conflicts: (BOOL)includeConflicts
lastSequence: (id)lastSequenceID
client: (id<TDChangeTrackerClient>)client;
@@ -68,6 +70,7 @@ typedef enum TDChangeTrackerMode {
@property (readonly) NSURL* changesFeedURL;
@property (readonly) NSString* changesFeedPath;
- (void) setUpstreamError: (NSString*)message;
+- (BOOL) receivedChange: (NSDictionary*)change;
- (BOOL) receivedChunk: (NSData*)chunk;
- (BOOL) receivedPollResponse: (NSData*)body;
- (void) stopped; // override this
View
37 Source/ChangeTracker/TDChangeTracker.m
@@ -19,6 +19,7 @@
#import "TDConnectionChangeTracker.h"
#import "TDSocketChangeTracker.h"
#import "TDMisc.h"
+#import "TDStatus.h"
@interface TDChangeTracker ()
@@ -33,6 +34,7 @@ @implementation TDChangeTracker
- (id)initWithDatabaseURL: (NSURL*)databaseURL
mode: (TDChangeTrackerMode)mode
+ conflicts: (BOOL)includeConflicts
lastSequence: (id)lastSequenceID
client: (id<TDChangeTrackerClient>)client {
NSParameterAssert(databaseURL);
@@ -42,22 +44,21 @@ - (id)initWithDatabaseURL: (NSURL*)databaseURL
if ([self class] == [TDChangeTracker class]) {
[self release];
// TDConnectionChangeTracker doesn't work in continuous due to some bug in CFNetwork.
- if (mode == kContinuous && [databaseURL.scheme.lowercaseString hasPrefix: @"http"]) {
- return (id) [[TDSocketChangeTracker alloc] initWithDatabaseURL: databaseURL
- mode: mode
- lastSequence: lastSequenceID
- client: client];
- } else {
- return (id) [[TDConnectionChangeTracker alloc] initWithDatabaseURL: databaseURL
- mode: mode
- lastSequence: lastSequenceID
- client: client];
- }
+ if (mode == kContinuous && [databaseURL.scheme.lowercaseString hasPrefix: @"http"])
+ self = [TDSocketChangeTracker alloc];
+ else
+ self = [TDConnectionChangeTracker alloc];
+ return [self initWithDatabaseURL: databaseURL
+ mode: mode
+ conflicts: includeConflicts
+ lastSequence: lastSequenceID
+ client: client];
}
_databaseURL = [databaseURL retain];
_client = client;
_mode = mode;
+ _includeConflicts = includeConflicts;
self.lastSequenceID = lastSequenceID;
}
return self;
@@ -72,6 +73,8 @@ - (NSString*) changesFeedPath {
NSMutableString* path;
path = [NSMutableString stringWithFormat: @"_changes?feed=%@&heartbeat=300000",
kModeNames[_mode]];
+ if (_includeConflicts)
+ [path appendString: @"&style=all_docs"];
if (_lastSequenceID)
[path appendFormat: @"&since=%@", TDEscapeURLParam([_lastSequenceID description])];
if (_filterName) {
@@ -95,7 +98,7 @@ - (NSURL*) changesFeedURL {
}
- (NSString*) description {
- return [NSString stringWithFormat: @"%@[%@]", [self class], self.databaseName];
+ return [NSString stringWithFormat: @"%@[%p %@]", [self class], self, self.databaseName];
}
- (void) dealloc {
@@ -117,7 +120,7 @@ - (NSURLCredential*) authCredential {
- (void) setUpstreamError: (NSString*)message {
Warn(@"%@: Server error: %@", self, message);
- self.error = [NSError errorWithDomain: @"TDChangeTracker" code: 502 userInfo: nil];
+ self.error = [NSError errorWithDomain: @"TDChangeTracker" code: kTDStatusUpstreamError userInfo: nil];
}
- (BOOL) start {
@@ -139,14 +142,18 @@ - (BOOL) receivedChange: (NSDictionary*)change {
if (![change isKindOfClass: [NSDictionary class]])
return NO;
id seq = [change objectForKey: @"seq"];
- if (!seq)
- return NO;
+ if (!seq) {
+ // If a continuous feed closes (e.g. if its database is deleted), the last line it sends
+ // will indicate the last_seq. This is normal, just ignore it and return success:
+ return [change objectForKey: @"last_seq"] != nil;
+ }
[_client changeTrackerReceivedChange: change];
self.lastSequenceID = seq;
return YES;
}
- (BOOL) receivedChunk: (NSData*)chunk {
+ LogTo(ChangeTracker, @"CHUNK: %@ %@", self, [chunk my_UTF8ToString]);
if (chunk.length > 1) {
id change = [TDJSON JSONObjectWithData: chunk options: 0 error: NULL];
if (![self receivedChange: change]) {
View
9 Source/ChangeTracker/TDConnectionChangeTracker.m
@@ -1,5 +1,5 @@
//
-// CouchConnectionChangeTracker.m
+// TDConnectionChangeTracker.m
// TouchDB
//
// Created by Jens Alfke on 12/1/11.
@@ -17,6 +17,7 @@
#import "TDConnectionChangeTracker.h"
#import "TDMisc.h"
+#import "TDStatus.h"
@implementation TDConnectionChangeTracker
@@ -78,11 +79,11 @@ - (void)connection:(NSURLConnection *)connection
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
- int status = (int) ((NSHTTPURLResponse*)response).statusCode;
+ TDStatus status = (TDStatus) ((NSHTTPURLResponse*)response).statusCode;
LogTo(ChangeTracker, @"%@: Got response, status %d", self, status);
- if (status >= 300) {
+ if (TDStatusIsError(status)) {
Warn(@"%@: Got status %i", self, status);
- self.error = TDHTTPError(status, self.changesFeedURL);
+ self.error = TDStatusToNSError(status, self.changesFeedURL);
[self stop];
}
}
View
4 Source/ChangeTracker/TDSocketChangeTracker.h
@@ -19,6 +19,10 @@
int _retryCount;
NSMutableData* _inputBuffer;
+ NSMutableData* _changeBuffer;
int _state;
+ bool _parsing;
+ bool _inputAvailable;
+ bool _atEOF;
}
@end
View
251 Source/ChangeTracker/TDSocketChangeTracker.m
@@ -17,21 +17,26 @@
#import "TDSocketChangeTracker.h"
#import "TDBase64.h"
+#import "MYBlockUtils.h"
#import <string.h>
+// Values of _state:
enum {
kStateStatus,
kStateHeaders,
- kStateChunks
+ kStateChunks,
};
-#define kMaxRetries 7
+#define kMaxRetries 6
+#define kInitialRetryDelay 0.2
+#define kReadLength 8192u
@implementation TDSocketChangeTracker
+
- (BOOL) start {
NSAssert(!_trackingInput, @"Already started");
NSAssert(_mode == kContinuous, @"TDSocketChangeTracker only supports continuous mode");
@@ -58,22 +63,26 @@ - (BOOL) start {
OS X 10.6.7, the delegate never receives any notification of a response. The workaround
is to act as a dumb HTTP parser and do the job ourselves. */
+ int port = _databaseURL.port.unsignedShortValue ?: 80;
#if TARGET_OS_IPHONE
CFReadStreamRef cfInputStream = NULL;
CFWriteStreamRef cfOutputStream = NULL;
CFStreamCreatePairWithSocketToHost(NULL,
(CFStringRef)_databaseURL.host,
- _databaseURL.port.intValue,
+ port,
&cfInputStream, &cfOutputStream);
if (!cfInputStream)
return NO;
_trackingInput = (NSInputStream*)cfInputStream;
_trackingOutput = (NSOutputStream*)cfOutputStream;
#else
+ NSString* hostname = _databaseURL.host;
+ if ($equal(hostname, @"localhost")) // for some reason connection fails if "localhost" used
+ hostname = @"127.0.0.1";
NSInputStream* input;
NSOutputStream* output;
- [NSStream getStreamsToHost: [NSHost hostWithName: _databaseURL.host]
- port: _databaseURL.port.intValue
+ [NSStream getStreamsToHost: [NSHost hostWithName: hostname]
+ port: port
inputStream: &input outputStream: &output];
if (!output)
return NO;
@@ -82,8 +91,9 @@ - (BOOL) start {
#endif
_state = kStateStatus;
+ _atEOF = _inputAvailable = _parsing = false;
- _inputBuffer = [[NSMutableData alloc] initWithCapacity: 1024];
+ _inputBuffer = [[NSMutableData alloc] initWithCapacity: kReadLength];
[_trackingOutput setDelegate: self];
[_trackingOutput scheduleInRunLoop: [NSRunLoop currentRunLoop] forMode: NSRunLoopCommonModes];
@@ -95,20 +105,28 @@ - (BOOL) start {
}
+- (void) clearConnection {
+ [_trackingInput close];
+ [_trackingInput release];
+ _trackingInput = nil;
+
+ [_trackingOutput close];
+ [_trackingOutput release];
+ _trackingOutput = nil;
+
+ [_inputBuffer release];
+ _inputBuffer = nil;
+ [_changeBuffer release];
+ _changeBuffer = nil;
+}
+
+
- (void) stop {
+ [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(start)
+ object: nil]; // cancel pending retries
if (_trackingInput || _trackingOutput) {
LogTo(ChangeTracker, @"%@: stop", self);
- [_trackingInput close];
- [_trackingInput release];
- _trackingInput = nil;
-
- [_trackingOutput close];
- [_trackingOutput release];
- _trackingOutput = nil;
-
- [_inputBuffer release];
- _inputBuffer = nil;
-
+ [self clearConnection];
[super stop];
}
}
@@ -122,23 +140,53 @@ - (BOOL) failUnparseable: (NSString*)line {
}
-- (BOOL) readLine {
- const char* start = _inputBuffer.bytes;
- const char* crlf = memmem(start, _inputBuffer.length, "\r\n", 2);
- if (!crlf)
- return NO; // Wait till we have a complete line
- ptrdiff_t lineLength = crlf - start;
- NSString* line = [[[NSString alloc] initWithBytes: start
- length: lineLength
- encoding: NSUTF8StringEncoding] autorelease];
- LogTo(ChangeTracker, @"%@: LINE: \"%@\"", self, line);
- if (line) {
+- (void) readChangeLine: (const void*)bytes
+ length: (NSUInteger)length
+ intoArray: (NSMutableArray*)changes
+{
+ if (_changeBuffer)
+ [_changeBuffer appendBytes: bytes length: length];
+ else
+ _changeBuffer = [[NSMutableData alloc] initWithBytes: bytes length: length];
+ while (_changeBuffer) { // abort loop if delegate calls -stop on me!
+ const void* start = _changeBuffer.bytes;
+ const void* eol = memchr(start, '\n', _changeBuffer.length);
+ if (!eol)
+ break;
+ NSData* line = [_changeBuffer subdataWithRange: NSMakeRange(0, eol-start)];
+ [_changeBuffer replaceBytesInRange: NSMakeRange(0, eol-start+1)
+ withBytes: NULL length: 0];
+ if (line.length > 0)
+ [changes addObject: line];
+ }
+}
+
+
+- (void) readLines {
+ NSMutableArray* changes = $marray();
+ const char* pos = _inputBuffer.bytes;
+ const char* end = pos + _inputBuffer.length;
+ BOOL keepGoing = YES;
+ while (keepGoing && pos < end && _inputBuffer) {
+ const char* lineStart = pos;
+ const char* crlf = memmem(pos, end-pos, "\r\n", 2);
+ if (!crlf)
+ break; // Wait till we have a complete line
+ ptrdiff_t lineLength = crlf - pos;
+ NSString* line = [[[NSString alloc] initWithBytes: pos
+ length: lineLength
+ encoding: NSUTF8StringEncoding] autorelease];
+ pos = crlf + 2;
+ if (!line) {
+ [self failUnparseable: @"invalid UTF-8"];
+ break;
+ }
+
switch (_state) {
case kStateStatus: {
// Read the HTTP response status line:
- if (![line hasPrefix: @"HTTP/1.1 200 "]) {
- return [self failUnparseable: line];
- }
+ if (![line hasPrefix: @"HTTP/1.1 200 "])
+ [self failUnparseable: line];
_state = kStateHeaders;
break;
}
@@ -153,45 +201,124 @@ - (BOOL) readLine {
break; // There's an empty line between chunks
NSScanner* scanner = [NSScanner scannerWithString: line];
unsigned chunkLength;
- if (![scanner scanHexInt: &chunkLength])
- return [self failUnparseable: line];
- if (_inputBuffer.length < (size_t)lineLength + 2 + chunkLength)
- return NO; // Don't read the chunk till it's complete
-
- NSData* chunk = [_inputBuffer subdataWithRange: NSMakeRange(lineLength + 2,
- chunkLength)];
- [_inputBuffer replaceBytesInRange: NSMakeRange(0, lineLength + 2 + chunkLength)
- withBytes: NULL length: 0];
- // Finally! Send the line to the database to parse:
- if ([self receivedChunk: chunk])
- return YES;
- else
- return [self failUnparseable: line];
+ if (![scanner scanHexInt: &chunkLength]) {
+ [self failUnparseable: line];
+ break;
+ }
+ if (pos + chunkLength > end) {
+ keepGoing = NO;
+ pos = lineStart;
+ break; // Don't read the chunk till it's complete
+ }
+ // Append the chunk to the current change line:
+ [self readChangeLine: pos length: chunkLength intoArray: changes];
+ pos += chunkLength;
}
}
- } else {
- return [self failUnparseable: line];
}
- // Remove the parsed line:
- [_inputBuffer replaceBytesInRange: NSMakeRange(0, lineLength + 2) withBytes: NULL length: 0];
- return YES;
+ // Remove the parsed lines:
+ [_inputBuffer replaceBytesInRange: NSMakeRange(0, pos - (const char*)_inputBuffer.bytes)
+ withBytes: NULL length: 0];
+
+ if (changes.count > 0)
+ [self asyncParseChangeLines: changes];
+}
+
+
+#pragma mark - ASYNC PARSING:
+
+
+- (void) asyncParseChangeLines: (NSArray*)lines {
+ static NSOperationQueue* sParseQueue;
+ if (!sParseQueue)
+ sParseQueue = [[NSOperationQueue alloc] init];
+
+ LogTo(ChangeTracker, @"%@: Async parsing %u changes...", self, lines.count);
+ Assert(!_parsing);
+ _parsing = true;
+ NSThread* resultThread = [NSThread currentThread];
+ [sParseQueue addOperationWithBlock: ^{
+ // Parse on background thread:
+ bool allParsed = true;
+ NSMutableArray* parsedChanges = [NSMutableArray arrayWithCapacity: lines.count];
+ for (NSData* line in lines) {
+ id change = [TDJSON JSONObjectWithData: line options: 0 error: NULL];
+ if (!change) {
+ Warn(@"TDSocketChangeTracker received unparseable change line from server: %@", [line my_UTF8ToString]);
+ allParsed = false;
+ break;
+ }
+ [parsedChanges addObject: change];
+ }
+ MYOnThread(resultThread, ^{
+ // Process change lines on original thread:
+ Assert(_parsing);
+ _parsing = false;
+ if (!_trackingInput)
+ return;
+ LogTo(ChangeTracker, @"%@: Notifying %u changes...", self, parsedChanges.count);
+ for (id change in parsedChanges) {
+ if (![self receivedChange: change]) {
+ [self failUnparseable: change];
+ break;
+ }
+ }
+ if (!allParsed) {
+ [self setUpstreamError: @"Unparseable change line"];
+ [self stop];
+ }
+
+ // Read more data if there is any, or stop if stream's at EOF:
+ if (_inputAvailable)
+ [self readFromInput];
+ else if (_atEOF)
+ [self stop];
+ });
+ }];
+}
+
+
+#pragma mark - STREAM HANDLING:
+
+
+- (void) readFromInput {
+ Assert(!_parsing);
+ Assert(_inputAvailable);
+ _inputAvailable = false;
+
+ uint8_t* buffer;
+ NSUInteger bufferLength;
+ NSInteger bytesRead;
+ if ([_trackingInput getBuffer: &buffer length: &bufferLength]) {
+ [_inputBuffer appendBytes: buffer length: bufferLength];
+ bytesRead = bufferLength;
+ } else {
+ uint8_t buffer[kReadLength];
+ bytesRead = [_trackingInput read: buffer maxLength: sizeof(buffer)];
+ if (bytesRead > 0)
+ [_inputBuffer appendBytes: buffer length: bytesRead];
+ }
+ LogTo(ChangeTracker, @"%@: read %ld bytes", self, (long)bytesRead);
+ [self readLines];
}
- (void) errorOccurred: (NSError*)error {
- [self stop];
if (++_retryCount <= kMaxRetries) {
- NSTimeInterval retryDelay = 0.2 * (1 << (_retryCount-1));
+ [self clearConnection];
+ NSTimeInterval retryDelay = kInitialRetryDelay * (1 << (_retryCount-1));
[self performSelector: @selector(start) withObject: nil afterDelay: retryDelay];
} else {
Warn(@"%@: Can't connect, giving up: %@", self, error);
+ [self stop];
self.error = error;
}
}
- (void) stream: (NSInputStream*)stream handleEvent: (NSStreamEvent)eventCode {
+ [[self retain] autorelease]; // Delegate calling -stop might otherwise dealloc me
switch (eventCode) {
case NSStreamEventHasSpaceAvailable: {
LogTo(ChangeTracker, @"%@: HasSpaceAvailable %@", self, stream);
@@ -208,23 +335,17 @@ - (void) stream: (NSInputStream*)stream handleEvent: (NSStreamEvent)eventCode {
}
case NSStreamEventHasBytesAvailable: {
LogTo(ChangeTracker, @"%@: HasBytesAvailable %@", self, stream);
- while ([stream hasBytesAvailable]) {
- uint8_t buffer[1024];
- NSInteger bytesRead = [stream read: buffer maxLength: sizeof(buffer)];
- if (bytesRead > 0) {
- [_inputBuffer appendBytes: buffer length: bytesRead];
- LogTo(ChangeTracker, @"%@: read %ld bytes", self, (long)bytesRead);
- }
- }
- while (_inputBuffer && [self readLine])
- ;
+ _inputAvailable = true;
+ // If still chewing on last bytes, don't eat any more yet
+ if (!_parsing)
+ [self readFromInput];
break;
}
case NSStreamEventEndEncountered:
LogTo(ChangeTracker, @"%@: EndEncountered %@", self, stream);
- if (_inputBuffer.length > 0)
- Warn(@"%@ connection closed with unparsed data in buffer", self);
- [self stop];
+ _atEOF = true;
+ if (!_parsing)
+ [self stop];
break;
case NSStreamEventErrorOccurred:
LogTo(ChangeTracker, @"%@: ErrorOccurred %@: %@", self, stream, stream.streamError);
View
36 Source/TDAttachment.h
@@ -0,0 +1,36 @@
+//
+// TDAttachment.h
+// TouchDB
+//
+// Created by Jens Alfke on 4/3/12.
+// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDDatabase+Attachments.h"
+#import "TDBlobStore.h"
+
+
+/** A simple container for attachment metadata. */
+@interface TDAttachment : NSObject
+{
+ @private
+ NSString* _name;
+ NSString* _contentType;
+ @public
+ // Yes, these are public. They're simple scalar values so it's not really worth
+ // creating accessor methods for them all.
+ TDBlobKey blobKey;
+ UInt64 length;
+ UInt64 encodedLength;
+ TDAttachmentEncoding encoding;
+ unsigned revpos;
+}
+
+- (id) initWithName: (NSString*)name contentType: (NSString*)contentType;
+
+@property (readonly, nonatomic) NSString* name;
+@property (readonly, nonatomic) NSString* contentType;
+
+@property (readonly) bool isValid;
+
+@end
View
58 Source/TDAttachment.m
@@ -0,0 +1,58 @@
+//
+// TDAttachment.m
+// TouchDB
+//
+// Created by Jens Alfke on 4/3/12.
+// Copyright (c) 2012 Couchbase, Inc. All rights reserved.
+//
+
+#import "TDAttachment.h"
+
+
+@implementation TDAttachment
+
+
+@synthesize name=_name, contentType=_contentType;
+
+
+- (id) initWithName: (NSString*)name contentType: (NSString*)contentType {
+ Assert(name);
+ self = [super init];
+ if (self) {
+ _name = [name copy];
+ _contentType = [contentType copy];
+ }
+ return self;
+}
+
+
+- (void)dealloc
+{
+ [_name release];
+ [_contentType release];
+ [super dealloc];
+}
+
+
+- (bool) isValid {
+ if (encoding) {
+ if (encodedLength == 0 && length > 0)
+ return false;
+ } else if (encodedLength > 0) {
+ return false;
+ }
+ if (revpos == 0)
+ return false;
+#if DEBUG
+ size_t i;
+ for (i=0; i<sizeof(TDBlobKey); i++)
+ if (blobKey.bytes[i])
+ return true;
+ return false;
+#else
+ return true;
+#endif
+}
+
+
+@end
View
5 Source/TDBlobStore.h
@@ -92,8 +92,9 @@ typedef struct {
/** After finishing, this is the key for looking up the blob through the TDBlobStore. */
@property (readonly) TDBlobKey blobKey;
-/** After finishing, this is the MD5 digest of the blob.
+/** After finishing, this is the MD5 digest of the blob, in base64 with an "md5-" prefix.
(This is useful for compatibility with CouchDB, which stores MD5 digests of attachments.) */
-@property (readonly) TDMD5Key MD5Digest;
+@property (readonly) NSString* MD5DigestString;
+@property (readonly) NSString* SHA1DigestString;
@end
View
13 Source/TDBlobStore.m
@@ -14,6 +14,7 @@
// and limitations under the License.
#import "TDBlobStore.h"
+#import "TDBase64.h"
#import "TDMisc.h"
#import <ctype.h>
@@ -232,7 +233,7 @@ - (NSString*) tempDir {
@implementation TDBlobStoreWriter
-@synthesize length=_length, blobKey=_blobKey, MD5Digest=_MD5Digest;
+@synthesize length=_length, blobKey=_blobKey;
- (id) initWithStore: (TDBlobStore*)store {
self = [super init];
@@ -279,6 +280,16 @@ - (void) finish {
MD5_Final(_MD5Digest.bytes, &_md5Ctx);
}
+- (NSString*) MD5DigestString {
+ return [@"md5-" stringByAppendingString: [TDBase64 encode: &_MD5Digest
+ length: sizeof(_MD5Digest)]];
+}
+
+- (NSString*) SHA1DigestString {
+ return [@"sha1-" stringByAppendingString: [TDBase64 encode: &_blobKey
+ length: sizeof(_blobKey)]];
+}
+
- (BOOL) install {
if (!_tempPath)
return YES; // already installed
View
4 Source/TDBody.m
@@ -80,8 +80,8 @@ - (NSData*) asPrettyJSON {
id props = self.asObject;
if (props) {
NSData* json = [TDJSON dataWithJSONObject: props
- options: TDJSONWritingPrettyPrinted
- error: NULL];
+ options: TDJSONWritingPrettyPrinted
+ error: NULL];
if (json) {
NSMutableData* mjson = [[json mutableCopy] autorelease];
[mjson appendBytes: "\n" length: 1];
View
44 Source/TDCanonicalJSON.h
@@ -0,0 +1,44 @@
+//
+// TDCanonicalJSON.h
+// TouchDB
+//
+// Created by Jens Alfke on 8/15/11.
+// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+/** Generates a canonical JSON form of an object tree, suitable for signing.
+ See algorithm at <http://wiki.apache.org/couchdb/SignedDocuments>. */
+@interface TDCanonicalJSON : NSObject
+{
+ @private
+ id _input;
+ NSString* _ignoreKeyPrefix;
+ NSArray* _whitelistedKeys;
+ NSMutableString* _output;
+}
+
+- (id) initWithObject: (id)object;
+
+/** If non-nil, dictionary keys beginning with this prefix will be ignored. */
+@property (nonatomic, copy) NSString* ignoreKeyPrefix;
+
+/** Keys to include even if they begin with the ignorePrefix. */
+@property (nonatomic, copy) NSArray* whitelistedKeys;
+
+/** Canonical JSON string from the input object tree.
+ This isn't directly useful for tasks like signing or generating digests; you probably want to use .canonicalData instead for that. */
+@property (readonly) NSString* canonicalString;
+
+/** Canonical form of UTF-8 encoded JSON data from the input object tree. */
+@property (readonly) NSData* canonicalData;
+
+
+/** Convenience method that instantiates a TDCanonicalJSON object and uses it to encode the object. */
++ (NSData*) canonicalData: (id)rootObject;
+
+/** Convenience method that instantiates a TDCanonicalJSON object and uses it to encode the object, returning a string. */
++ (NSString*) canonicalString: (id)rootObject;
+
+@end
View
288 Source/TDCanonicalJSON.m
@@ -0,0 +1,288 @@
+//
+// TDCanonicalJSON.m
+// TouchDB
+//
+// Created by Jens Alfke on 8/15/11.
+// Copyright (c) 2011 Couchbase, Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License. You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software distributed under the
+// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+// either express or implied. See the License for the specific language governing permissions
+// and limitations under the License.
+
+#import "TDCanonicalJSON.h"
+#import <math.h>
+
+
+@interface TDCanonicalJSON ()
+- (void) encode: (id)object;
+@end
+
+
+@implementation TDCanonicalJSON
+
+
+- (id) initWithObject: (id)object {
+ self = [super init];
+ if (self) {
+ _input = [object retain];
+ }
+ return self;
+}
+
+
+- (void)dealloc {
+ [_ignoreKeyPrefix release];
+ [_whitelistedKeys release];
+ [_output release];
+ [_input release];
+ [super dealloc];
+}
+
+
+@synthesize ignoreKeyPrefix=_ignoreKeyPrefix, whitelistedKeys=_whitelistedKeys;
+
+
+- (void) encodeString: (NSString*)string {
+ static NSCharacterSet* kCharsToQuote;
+ if (!kCharsToQuote) {
+ NSMutableCharacterSet* chars = (id)[NSMutableCharacterSet characterSetWithRange: NSMakeRange(0, 32)];
+ [chars addCharactersInString: @"\"\\"];
+ kCharsToQuote = [chars copy];
+ }
+
+ [_output appendString: @"\""];
+ NSRange remainder = {0, string.length};
+ while (remainder.length > 0) {
+ NSRange quote = [string rangeOfCharacterFromSet: kCharsToQuote options: 0 range: remainder];
+ if (quote.length == 0)
+ quote.location = string.length;
+ NSUInteger nChars = quote.location - remainder.location;
+ [_output appendString: [string substringWithRange:
+ NSMakeRange(remainder.location, nChars)]];
+ if (quote.length > 0) {
+ unichar ch = [string characterAtIndex: quote.location];
+ NSString* escaped;
+ switch (ch) {
+ case '"':
+ escaped = @"\\\"";
+ break;
+ case '\\':
+ escaped = @"\\\\";
+ break;
+ case '\r':
+ escaped = @"\\r";
+ break;
+ case '\n':
+ escaped = @"\\n";
+ break;
+ default:
+ escaped = [NSString stringWithFormat: @"\\u%04x", ch];
+ break;
+ }
+ [_output appendString: escaped];
+ ++nChars;
+ }
+ remainder.location += nChars;
+ remainder.length -= nChars;
+ }
+ [_output appendString: @"\""];
+}
+
+
+- (void) encodeNumber: (NSNumber*)number {
+ const char* encoding = number.objCType;
+ if (encoding[0] == 'c')
+ [_output appendString:[number boolValue] ? @"true" : @"false"];
+ else
+ [_output appendString:[number stringValue]];
+}
+
+
+- (void) encodeArray: (NSArray*)array {
+ [_output appendString: @"["];
+ BOOL first = YES;
+ for (id item in array) {
+ if (first)
+ first = NO;
+ else
+ [_output appendString: @","];
+ [self encode: item];
+ }
+ [_output appendString: @"]"];
+}
+
+
+static NSComparisonResult compareCanonStrings( id s1, id s2, void *context) {
+ return [s1 compare: s2 options: NSLiteralSearch];
+ /* Alternate implementation in case NSLiteralSearch turns out to be inappropriate:
+ NSUInteger len1 = [s1 length], len2 = [s2 length];
+ unichar chars1[len1], chars2[len2]; //FIX: Will crash (stack overflow) on v. long strings
+ [s1 getCharacters: chars1 range: NSMakeRange(0, len1)];
+ [s2 getCharacters: chars2 range: NSMakeRange(0, len2)];
+ NSUInteger minLen = MIN(len1, len2);
+ for (NSUInteger i=0; i<minLen; i++) {
+ if (chars1[i] > chars2[i])
+ return 1;
+ else if (chars1[i] < chars2[i])
+ return -1;
+ }
+ // All chars match, so the longer string wins
+ return (NSInteger)len1 - (NSInteger)len2;
+ */
+}
+
+
+- (void) encodeDictionary: (NSDictionary*)dict {
+ [_output appendString: @"{"];
+ NSArray* keys = [[dict allKeys] sortedArrayUsingFunction: &compareCanonStrings context: NULL];
+ BOOL first = YES;
+ for (NSString* key in keys) {
+ Assert([key isKindOfClass: [NSString class]], @"Can't encode %@ as dict key in JSON",
+ [key class]);
+ if (_ignoreKeyPrefix && [key hasPrefix: _ignoreKeyPrefix]
+ && ![_whitelistedKeys containsObject: key])
+ continue;
+ if (first)
+ first = NO;
+ else
+ [_output appendString: @","];
+ [self encodeString: key];
+ [_output appendString: @":"];
+ [self encode: [dict objectForKey: key]];
+ }
+ [_output appendString: @"}"];
+}
+
+
+- (void) encode: (id)object {
+ if ([object isKindOfClass: [NSString class]]) {
+ [self encodeString: object];
+ } else if ([object isKindOfClass: [NSNumber class]]) {
+ [self encodeNumber: object];
+ } else if ([object isKindOfClass: [NSNull class]]) {
+ [_output appendString: @"null"];
+ } else if ([object isKindOfClass: [NSDictionary class]]) {
+ [self encodeDictionary: object];
+ } else if ([object isKindOfClass: [NSArray class]]) {
+ [self encodeArray: object];
+ } else {
+ Assert(NO, @"Can't encode instances of %@ as JSON", [object class]);
+ }
+}
+
+
+- (void) encode {
+ if (!_output) {
+ _output = [[NSMutableString alloc] init];
+ [self encode: _input];
+ }
+}
+
+
+- (NSString*) canonicalString {
+ [self encode];
+ return [[_output copy] autorelease];
+}
+
+
+- (NSData*) canonicalData {
+ [self encode];
+ return [_output dataUsingEncoding: NSUTF8StringEncoding];
+}
+
+
++ (NSString*) canonicalString: (id)rootObject {
+ TDCanonicalJSON* encoder = [[self alloc] initWithObject: rootObject];
+ NSString* result = encoder.canonicalString;
+ [encoder release];
+ return result;
+}
+
+
++ (NSData*) canonicalData: (id)rootObject {
+ TDCanonicalJSON* encoder = [[self alloc] initWithObject: rootObject];
+ NSData* result = encoder.canonicalData;
+ [encoder release];
+ return result;
+}
+
+
+@end
+
+
+
+#if DEBUG
+
+static void roundtrip( id obj ) {
+ NSData* json = [TDCanonicalJSON canonicalData: obj];
+ Log(@"%@ --> `%@`", [obj description], [json my_UTF8ToString]);
+ NSError* error;
+ id reconstituted = [NSJSONSerialization JSONObjectWithData: json options:NSJSONReadingAllowFragments error: &error];
+ CAssert(reconstituted, @"Canonical JSON `%@` was unparseable: %@",
+ [json my_UTF8ToString], error);
+ CAssertEqual(reconstituted, obj);
+}
+
+static void roundtripFloat( double n ) {
+ NSData* json = [TDCanonicalJSON canonicalData: [NSNumber numberWithDouble: n]];
+ NSError* error;
+ id reconstituted = [NSJSONSerialization JSONObjectWithData: json options:NSJSONReadingAllowFragments error: &error];
+ CAssert(reconstituted, @"`%@` was unparseable: %@",
+ [json my_UTF8ToString], error);
+ double delta = [reconstituted doubleValue] / n - 1.0;
+ Log(@"%g --> `%@` (error = %g)", n, [json my_UTF8ToString], delta);
+ CAssert(fabs(delta) < 1.0e-15, @"`%@` had floating point roundoff error of %g (%g vs %g)",
+ [json my_UTF8ToString], delta, [reconstituted doubleValue], n);
+}
+
+TestCase(TDCanonicalJSON_RoundTrip) {
+ roundtrip($true);
+ roundtrip($false);
+ roundtrip($null);
+
+ roundtrip([NSNumber numberWithInt: 0]);
+ roundtrip([NSNumber numberWithInt: INT_MAX]);
+ roundtrip([NSNumber numberWithInt: INT_MIN]);
+ roundtrip([NSNumber numberWithUnsignedInt: UINT_MAX]);
+ roundtrip([NSNumber numberWithLongLong: INT64_MAX]);
+ roundtrip([NSNumber numberWithUnsignedLongLong: UINT64_MAX]);
+
+ roundtripFloat(111111.111111);
+ roundtripFloat(M_PI);
+ roundtripFloat(6.02e23);
+ roundtripFloat(1.23456e-18);
+ roundtripFloat(1.0e-37);
+ roundtripFloat(UINT_MAX);
+ roundtripFloat(UINT64_MAX);
+ roundtripFloat(UINT_MAX + 0.01);
+ roundtripFloat(1.0e38);
+
+ roundtrip(@"");
+ roundtrip(@"ordinary string");
+ roundtrip(@"\\");
+ roundtrip(@"xx\\");
+ roundtrip(@"\\xx");
+ roundtrip(@"\"\\");
+ roundtrip(@"\\.\"");
+ roundtrip(@"...\\.\"...");
+ roundtrip(@"...\\..\"...");
+ roundtrip(@"\r\nHELO\r \tTHER");
+ roundtrip(@"\037wow\037");
+ roundtrip(@"\001");
+ roundtrip(@"\u1234");
+
+ roundtrip($array());
+ roundtrip($array($array()));
+ roundtrip($array(@"foo", @"bar", $null));
+
+ roundtrip($dict());
+ roundtrip($dict({@"key", @"value"}));
+ roundtrip($dict({@"\"key\"", $false}));
+ roundtrip($dict({@"\"key\"", $false}, {@"", $dict()}));
+}
+
+#endif
View
9 Source/TDCollateJSON.m
@@ -265,11 +265,10 @@ int TDCollateJSON(void *context,
#if DEBUG
// encodes an object to a C string in JSON format. JSON fragments are allowed.
static const char* encode(id obj) {
- NSArray* wrapped = $array(obj);
- NSData* data = [TDJSON dataWithJSONObject: wrapped options: 0 error: NULL];
- CAssert(data);
- data = [data subdataWithRange: NSMakeRange(1, data.length-2)]; // strip the brackets
- return [[data my_UTF8ToString] UTF8String];
+ NSString* str = [TDJSON stringWithJSONObject: obj
+ options: TDJSONWritingAllowFragments error: NULL];
+ CAssert(str);
+ return [str UTF8String];
}
static void testEscape(const char* source, char decoded) {
View
21 Source/TDDatabase+Attachments.h
@@ -7,7 +7,7 @@
//
#import <TouchDB/TDDatabase.h>
-@class TDBlobStoreWriter;
+@class TDBlobStoreWriter, TDMultipartWriter;
/** Types of encoding/compression of stored attachments. */
@@ -22,16 +22,27 @@ typedef enum {
/** Creates a TDBlobStoreWriter object that can be used to stream an attachment to the store. */
- (TDBlobStoreWriter*) attachmentWriter;
+/** Creates TDAttachment objects from the revision's '_attachments' property. */
+- (NSDictionary*) attachmentsFromRevision: (TDRevision*)rev
+ status: (TDStatus*)outStatus;
+
/** Given a newly-added revision, adds the necessary attachment rows to the database and stores inline attachments into the blob store. */
-- (TDStatus) processAttachmentsForRevision: (TDRevision*)rev
- withParentSequence: (SequenceNumber)parentSequence;
+- (TDStatus) processAttachments: (NSDictionary*)attachments
+ forRevision: (TDRevision*)rev
+ withParentSequence: (SequenceNumber)parentSequence;
/** Constructs an "_attachments" dictionary for a revision, to be inserted in its JSON body. */
- (NSDictionary*) getAttachmentDictForSequence: (SequenceNumber)sequence
options: (TDContentOptions)options;
-/** Modifies a TDRevision's body by changing all attachments with revpos < minRevPos into stubs. */
-+ (void) stubOutAttachmentsIn: (TDRevision*)rev beforeRevPos: (int)minRevPos;
+/** Modifies a TDRevision's _attachments dictionary by changing all attachments with revpos < minRevPos into stubs; and if 'attachmentsFollow' is true, the remaining attachments will be modified to _not_ be stubs but include a "follows" key instead of a body. */
++ (void) stubOutAttachmentsIn: (TDRevision*)rev
+ beforeRevPos: (int)minRevPos
+ attachmentsFollow: (BOOL)attachmentsFollow;
+
+/** Generates a MIME multipart writer for a revision, with separate body parts for each attachment whose "follows" property is set. */
+- (TDMultipartWriter*) multipartWriterForRevision: (TDRevision*)rev
+ contentType: (NSString*)contentType;
/** Returns the content and metadata of an attachment.
If you pass NULL for the 'outEncoding' parameter, it signifies that you don't care about encodings and just want the 'real' data, so it'll be decoded for you. */
View
364 Source/TDDatabase+Attachments.m
@@ -28,7 +28,10 @@
#import "TDDatabase+Insertion.h"
#import "TDBase64.h"
#import "TDBlobStore.h"
+#import "TDAttachment.h"
#import "TDBody.h"
+#import "TDMultipartWriter.h"
+#import "TDMisc.h"
#import "TDInternal.h"
#import "CollectionUtils.h"
@@ -42,8 +45,6 @@
#define kBigAttachmentLength (16*1024)
-NSString* const kTDAttachmentBlobKeyProperty = @"__tdblobkey__";
-
@implementation TDDatabase (Attachments)
@@ -60,42 +61,76 @@ - (void) rememberAttachmentWritersForDigests: (NSDictionary*)blobsByDigests {
}
-- (NSData*) keyForAttachment: (NSData*)contents {
- Assert(contents);
- TDBlobKey key;
- if (![_attachments storeBlob: contents creatingKey: &key])
+// This is ONLY FOR TESTS (see TDMultipartDownloader.m)
+#if DEBUG
+- (id) attachmentWriterForAttachment: (NSDictionary*)attachment {
+ NSString* digest = $castIf(NSString, [attachment objectForKey: @"digest"]);
+ if (!digest)
return nil;
- return [NSData dataWithBytes: &key length: sizeof(key)];
+ return [_pendingAttachmentsByDigest objectForKey: digest];
+}
+#endif
+
+
+- (TDStatus) installAttachment: (TDAttachment*)attachment
+ forInfo: (NSDictionary*)attachInfo {
+ NSString* digest = $castIf(NSString, [attachInfo objectForKey: @"digest"]);
+ if (!digest)
+ return kTDStatusBadAttachment;
+ id writer = [_pendingAttachmentsByDigest objectForKey: digest];
+
+ if ([writer isKindOfClass: [TDBlobStoreWriter class]]) {
+ // Found a blob writer, so install the blob:
+ if (![writer install])
+ return kTDStatusAttachmentError;
+ attachment->blobKey = [writer blobKey];
+ attachment->length = [writer length];
+
+ // Remove the writer but leave the blob-key behind for future use:
+ NSData* keyData = [NSData dataWithBytes: &attachment->blobKey length: sizeof(TDBlobKey)];
+ [_pendingAttachmentsByDigest setObject: keyData forKey: digest];
+ return kTDStatusOK;
+
+ } else if ([writer isKindOfClass: [NSData class]]) {
+ // This attachment was already added, but the key was left behind in the dictionary:
+ attachment->blobKey = *(TDBlobKey*)[writer bytes];
+ NSNumber* lengthObj = $castIf(NSNumber, [attachInfo objectForKey: @"length"]);
+ if (!lengthObj)
+ return kTDStatusBadAttachment;
+ attachment->length = lengthObj.unsignedLongLongValue;
+ return kTDStatusOK;
+
+ } else {
+ return kTDStatusBadAttachment;
+ }
+}
+
+
+- (BOOL) storeBlob: (NSData*)blob creatingKey: (TDBlobKey*)outKey {
+ return [_attachments storeBlob: blob creatingKey: outKey];
}
-- (TDStatus) insertAttachmentWithKey: (NSData*)keyData
- forSequence: (SequenceNumber)sequence
- named: (NSString*)name
- type: (NSString*)contentType
- encoding: (TDAttachmentEncoding)encoding
- length: (UInt64)length
- encodedLength: (UInt64)encodedLength
- revpos: (unsigned)revpos
+- (TDStatus) insertAttachment: (TDAttachment*)attachment
+ forSequence: (SequenceNumber)sequence
{
Assert(sequence > 0);
- Assert(name);
- Assert(!encoding || length==0 || encodedLength > 0);
- if(!keyData)
- return 500;
- if (encodedLength > length)
+ Assert(attachment.isValid);
+ NSData* keyData = [NSData dataWithBytes: &attachment->blobKey length: sizeof(TDBlobKey)];
+ if (attachment->encodedLength > attachment->length)
Warn(@"Encoded attachment bigger than original: %llu > %llu for key %@",
- encodedLength, length, keyData);
- id encodedLengthObj = encoding ? $object(encodedLength) : nil;
+ attachment->encodedLength, attachment->length, keyData);
+ id encodedLengthObj = attachment->encoding ? $object(attachment->encodedLength) : nil;
if (![_fmdb executeUpdate: @"INSERT INTO attachments "
"(sequence, filename, key, type, encoding, length, encoded_length, revpos) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
- $object(sequence), name, keyData, contentType,
- $object(encoding), $object(length), encodedLengthObj,
- $object(revpos)]) {
- return 500;
+ $object(sequence), attachment.name, keyData,
+ attachment.contentType, $object(attachment->encoding),
+ $object(attachment->length), encodedLengthObj,
+ $object(attachment->revpos)]) {
+ return kTDStatusDBError;
}
- return 201;
+ return kTDStatusCreated;
}
@@ -107,23 +142,23 @@ - (TDStatus) copyAttachmentNamed: (NSString*)name
Assert(toSequence > 0);
Assert(toSequence > fromSequence);
if (fromSequence <= 0)
- return 404;
+ return kTDStatusNotFound;
if (![_fmdb executeUpdate: @"INSERT INTO attachments "
"(sequence, filename, key, type, encoding, encoded_Length, length, revpos) "
"SELECT ?, ?, key, type, encoding, encoded_Length, length, revpos "
"FROM attachments WHERE sequence=? AND filename=?",
$object(toSequence), name,
$object(fromSequence), name]) {
- return 500;
+ return kTDStatusDBError;
}
if (_fmdb.changes == 0) {
// Oops. This means a glitch in our attachment-management or pull code,
// or else a bug in the upstream server.
Warn(@"Can't find inherited attachment '%@' from seq#%lld to copy to #%lld",
name, fromSequence, toSequence);
- return 404; // Fail if there is no such attachment on fromSequence
+ return kTDStatusNotFound; // Fail if there is no such attachment on fromSequence
}
- return 200;
+ return kTDStatusOK;
}
@@ -150,29 +185,32 @@ - (NSData*) getAttachmentForSequence: (SequenceNumber)sequence
Assert(sequence > 0);
Assert(filename);
NSData* contents = nil;
- *outStatus = 500;
FMResultSet* r = [_fmdb executeQuery:
@"SELECT key, type, encoding FROM attachments WHERE sequence=? AND filename=?",
$object(sequence), filename];
- if (!r)
+ if (!r) {
+ *outStatus = kTDStatusDBError;
return nil;
+ }
@try {
if (![r next]) {
- *outStatus = 404;
+ *outStatus = kTDStatusNotFound;
return nil;
}
NSData* keyData = [r dataNoCopyForColumnIndex: 0];
if (keyData.length != sizeof(TDBlobKey)) {
Warn(@"%@: Attachment %lld.'%@' has bogus key size %d",
self, sequence, filename, keyData.length);
+ *outStatus = kTDStatusCorruptError;
return nil;
}
contents = [_attachments blobForKey: *(TDBlobKey*)keyData.bytes];
if (!contents) {
Warn(@"%@: Failed to load attachment %lld.'%@'", self, sequence, filename);
+ *outStatus = kTDStatusCorruptError;
return nil;
}
- *outStatus = 200;
+ *outStatus = kTDStatusOK;
if (outType)
*outType = [r stringForColumnIndex: 1];
@@ -267,9 +305,11 @@ - (NSInputStream*) inputStreamForAttachmentDict: (NSDictionary*)attachmentDict
}
-+ (void) stubOutAttachmentsIn: (TDRevision*)rev beforeRevPos: (int)minRevPos
++ (void) stubOutAttachmentsIn: (TDRevision*)rev
+ beforeRevPos: (int)minRevPos
+ attachmentsFollow: (BOOL)attachmentsFollow
{
- if (minRevPos <= 1)
+ if (minRevPos <= 1 && !attachmentsFollow)
return;
NSDictionary* properties = rev.properties;
NSMutableDictionary* editedProperties = nil;
@@ -278,21 +318,33 @@ + (void) stubOutAttachmentsIn: (TDRevision*)rev beforeRevPos: (int)minRevPos
for (NSString* name in attachments) {
NSDictionary* attachment = [attachments objectForKey: name];
int revPos = [[attachment objectForKey: @"revpos"] intValue];
- if (revPos > 0 && revPos < minRevPos && ![attachment objectForKey: @"stub"]) {
- // Strip this attachment's body. First make its dictionary mutable:
+ bool includeAttachment = (revPos == 0 || revPos >= minRevPos);
+ bool stubItOut = !includeAttachment && ![attachment objectForKey: @"stub"];
+ bool addFollows = includeAttachment && attachmentsFollow
+ && ![attachment objectForKey: @"follows"];
+ if (stubItOut || addFollows) {
+ // Need to modify attachment entry:
if (!editedProperties) {
+ // Make the document properties and _attachments dictionary mutable:
editedProperties = [[properties mutableCopy] autorelease];
editedAttachments = [[attachments mutableCopy] autorelease];
[editedProperties setObject: editedAttachments forKey: @"_attachments"];
}
- // ...then remove the 'data' and 'follows' key:
NSMutableDictionary* editedAttachment = [[attachment mutableCopy] autorelease];
[editedAttachment removeObjectForKey: @"data"];
- [editedAttachment removeObjectForKey: @"follows"];
- [editedAttachment setObject: $true forKey: @"stub"];
+ if (stubItOut) {
+ // ...then remove the 'data' and 'follows' key:
+ [editedAttachment removeObjectForKey: @"follows"];
+ [editedAttachment setObject: $true forKey: @"stub"];
+ LogTo(SyncVerbose, @"Stubbed out attachment %@/'%@': revpos %d < %d",
+ rev, name, revPos, minRevPos);
+ } else if (addFollows) {
+ [editedAttachment removeObjectForKey: @"stub"];
+ [editedAttachment setObject: $true forKey: @"follows"];
+ LogTo(SyncVerbose, @"Added 'follows' for attachment %@/'%@': revpos %d >= %d",
+ rev, name, revPos, minRevPos);
+ }
[editedAttachments setObject: editedAttachment forKey: name];
- LogTo(SyncVerbose, @"Stubbed out attachment %@/'%@': revpos %d < %d",
- rev, name, revPos, minRevPos);
}
}
if (editedProperties)
@@ -300,85 +352,105 @@ + (void) stubOutAttachmentsIn: (TDRevision*)rev beforeRevPos: (int)minRevPos
}
-- (TDStatus) processAttachmentsForRevision: (TDRevision*)rev
- withParentSequence: (SequenceNumber)parentSequence
+- (NSDictionary*) attachmentsFromRevision: (TDRevision*)rev
+ status: (TDStatus*)outStatus
{
- Assert(rev);
- SequenceNumber newSequence = rev.sequence;
- Assert(newSequence > 0);
- Assert(newSequence > parentSequence);
-
// If there are no attachments in the new rev, there's nothing to do:
- NSDictionary* newAttachments = [rev.properties objectForKey: @"_attachments"];
- if (newAttachments.count == 0 || rev.deleted)
- return 200;
+ NSDictionary* revAttachments = [rev.properties objectForKey: @"_attachments"];
+ if (revAttachments.count == 0 || rev.deleted) {
+ *outStatus = kTDStatusOK;
+ return [NSDictionary dictionary];
+ }
- for (NSString* name in newAttachments) {
- TDStatus status;
- NSDictionary* newAttach = [newAttachments objectForKey: name];
- NSData* blobKey = nil;
- UInt64 length;
-
- NSString* newContentsBase64 = $castIf(NSString, [newAttach objectForKey: @"data"]);
+ TDStatus status = kTDStatusOK;
+ NSMutableDictionary* attachments = $mdict();
+ for (NSString* name in revAttachments) {
+ // Create a TDAttachment object:
+ NSDictionary* attachInfo = [revAttachments objectForKey: name];
+ NSString* contentType = $castIf(NSString, [attachInfo objectForKey: @"content_type"]);
+ TDAttachment* attachment = [[[TDAttachment alloc] initWithName: name
+ contentType: contentType] autorelease];
+
+ NSString* newContentsBase64 = $castIf(NSString, [attachInfo objectForKey: @"data"]);
if (newContentsBase64) {
// If there's inline attachment data, decode and store it:
@autoreleasepool {
NSData* newContents = [TDBase64 decode: newContentsBase64];
- if (!newContents)
- return 400;
- length = newContents.length;
- blobKey = [[self keyForAttachment: newContents] retain]; // store attachment!
+ if (!newContents) {
+ status = kTDStatusBadEncoding;
+ break;
+ }
+ attachment->length = newContents.length;
+ if (![self storeBlob: newContents creatingKey: &attachment->blobKey]) {
+ status = kTDStatusAttachmentError;
+ break;
+ }
}
- [blobKey autorelease];
- } else if ([[newAttach objectForKey: @"follows"] isEqual: $true]) {
+ } else if ([[attachInfo objectForKey: @"follows"] isEqual: $true]) {
// "follows" means the uploader provided the attachment in a separate MIME part.
// This means it's already been registered in _pendingAttachmentsByDigest;
// I just need to look it up by its "digest" property and install it into the store:
- NSString* digest = $castIf(NSString, [newAttach objectForKey: @"digest"]);
- if (!digest)
- return 400;
- TDBlobStoreWriter *writer = [_pendingAttachmentsByDigest objectForKey: digest];
- if (![writer install])
- return 500;
- TDBlobKey key = writer.blobKey;
- blobKey = [NSData dataWithBytes: &key length: sizeof(key)];
- length = writer.length;
+ status = [self installAttachment: attachment forInfo: attachInfo];
+ if (TDStatusIsError(status))
+ break;
+ } else {
+ // This item is just a stub; skip it
+ continue;
}
- if (blobKey) {
- // New item contains data, so insert it.
- // First determine the revpos, i.e. generation # this was added in. Usually this is
- // implicit, but a rev being pulled in replication will have it set already.
- unsigned generation = rev.generation;
- Assert(generation > 0, @"Missing generation in rev %@", rev);
- NSNumber* revposObj = $castIf(NSNumber, [newAttach objectForKey: @"revpos"]);
- unsigned revpos = revposObj ? (unsigned)revposObj.intValue : generation;
- if (revpos > generation)
- return 400;
-
- // Handle encoded attachment:
- TDAttachmentEncoding encoding = kTDAttachmentEncodingNone;
- UInt64 encodedLength = 0;
- NSString* encodingStr = [newAttach objectForKey: @"encoding"];
- if (encodingStr) {
- if ($equal(encodingStr, @"gzip"))
- encoding = kTDAttachmentEncodingGZIP;
- else
- return 400;
-
- encodedLength = length;
- length = $castIf(NSNumber, [newAttach objectForKey: @"length"]).unsignedLongLongValue;
+ // Handle encoded attachment:
+ NSString* encodingStr = [attachInfo objectForKey: @"encoding"];
+ if (encodingStr) {
+ if ($equal(encodingStr, @"gzip"))
+ attachment->encoding = kTDAttachmentEncodingGZIP;
+ else {
+ status = kTDStatusBadEncoding;
+ break;
}
+
+ attachment->encodedLength = attachment->length;
+ attachment->length = $castIf(NSNumber, [attachInfo objectForKey: @"length"]).unsignedLongLongValue;
+ }
+
+ attachment->revpos = $castIf(NSNumber, [attachInfo objectForKey: @"revpos"]).unsignedIntValue;
+ [attachments setObject: attachment forKey: name];
+ }
+
+ *outStatus = status;
+ return status<300 ? attachments : nil;
+}
+
+
+- (TDStatus) processAttachments: (NSDictionary*)attachments
+ forRevision: (TDRevision*)rev
+ withParentSequence: (SequenceNumber)parentSequence
+{
+ Assert(rev);
+
+ // If there are no attachments in the new rev, there's nothing to do:
+ NSDictionary* revAttachments = [rev.properties objectForKey: @"_attachments"];
+ if (revAttachments.count == 0 || rev.deleted)
+ return kTDStatusOK;
+
+ SequenceNumber newSequence = rev.sequence;
+ Assert(newSequence > 0);
+ Assert(newSequence > parentSequence);
+ unsigned generation = rev.generation;
+ Assert(generation > 0, @"Missing generation in rev %@", rev);
+
+ for (NSString* name in revAttachments) {
+ TDStatus status;
+ TDAttachment* attachment = [attachments objectForKey: name];
+ if (attachment) {
+ // Determine the revpos, i.e. generation # this was added in. Usually this is
+ // implicit, but a rev being pulled in replication will have it set already.
+ if (attachment->revpos == 0)
+ attachment->revpos = generation;
+ else if (attachment->revpos > generation)
+ return kTDStatusBadAttachment;
// Finally insert the attachment:
- status = [self insertAttachmentWithKey: blobKey
- forSequence: newSequence
- named: name
- type: [newAttach objectForKey: @"content_type"]
- encoding: encoding
- length: length
- encodedLength: encodedLength
- revpos: revpos];
+ status = [self insertAttachment: attachment forSequence: newSequence];
} else {
// It's just a stub, so copy the previous revision's attachment entry:
//? Should I enforce that the type and digest (if any) match?
@@ -386,10 +458,32 @@ - (TDStatus) processAttachmentsForRevision: (TDRevision*)rev
fromSequence: parentSequence
toSequence: newSequence];
}
- if (status >= 300)
+ if (TDStatusIsError(status))
return status;
}
- return 200;
+ return kTDStatusOK;
+}
+
+
+- (TDMultipartWriter*) multipartWriterForRevision: (TDRevision*)rev
+ contentType: (NSString*)contentType
+{
+ TDMultipartWriter* writer = [[TDMultipartWriter alloc] initWithContentType: contentType
+ boundary: nil];
+ [writer setNextPartsHeaders: $dict({@"Content-Type", @"application/json"})];
+ [writer addData: rev.asJSON];
+ NSDictionary* attachments = [rev.properties objectForKey: @"_attachments"];
+ for (NSString* attachmentName in attachments) {
+ NSDictionary* attachment = [attachments objectForKey: attachmentName];
+ if ([attachment objectForKey: @"follows"]) {
+ UInt64 length;
+ NSInputStream *stream = [self inputStreamForAttachmentDict: attachment length: &length];
+ NSString* disposition = $sprintf(@"attachment; filename=%@", TDQuoteString(attachmentName));
+ [writer setNextPartsHeaders: $dict({@"Content-Disposition", disposition})];
+ [writer addStream: stream length: length];
+ }
+ }
+ return [writer autorelease];
}
@@ -401,7 +495,7 @@ - (TDRevision*) updateAttachment: (NSString*)filename
revID: (NSString*)oldRevID
status: (TDStatus*)outStatus
{
- *outStatus = 400;
+ *outStatus = kTDStatusBadAttachment;
if (filename.length == 0 || (body && !contentType) || (oldRevID && !docID) || (body && !docID))
return nil;
@@ -411,14 +505,14 @@ - (TDRevision*) updateAttachment: (NSString*)filename
if (oldRevID) {
// Load existing revision if this is a replacement:
*outStatus = [self loadRevisionBody: oldRev options: 0];
- if (*outStatus >= 300) {
- if (*outStatus == 404 && [self existsDocumentWithID: docID revisionID: nil])
- *outStatus = 409; // if some other revision exists, it's a conflict
+ if (TDStatusIsError(*outStatus)) {
+ if (*outStatus == kTDStatusNotFound && [self existsDocumentWithID: docID revisionID: nil])
+ *outStatus = kTDStatusConflict; // if some other revision exists, it's a conflict
return nil;
}
NSDictionary* attachments = [oldRev.properties objectForKey: @"_attachments"];
if (!body && ![attachments objectForKey: filename]) {
- *outStatus = 404;
+ *outStatus = kTDStatusAttachmentNotFound;
return nil;
}
// Remove the _attachments stubs so putRevision: doesn't copy the rows for me
@@ -448,35 +542,39 @@ - (TDRevision*) updateAttachment: (NSString*)filename
"FROM attachments WHERE sequence=? AND filename != ?",
$object(newRev.sequence), $object(oldRev.sequence),
filename]) {
- *outStatus = 500;
+ *outStatus = kTDStatusDBError;
return nil;
}
}
if (body) {
// If not deleting, add a new attachment entry:
- UInt64 length = body.length, encodedLength = 0;
+ TDAttachment* attachment = [[TDAttachment alloc] initWithName: filename
+ contentType: contentType];
+ [attachment autorelease];
+ attachment->length = body.length;
+ attachment->encoding = encoding;
+ attachment->revpos = newRev.generation;
if (encoding) {
- encodedLength = length;
- length = [self decodeAttachment: body encoding: encoding].length;
- if (length == 0 && encodedLength > 0) {
- *outStatus = 400; // failed to decode
+ attachment->encodedLength = attachment->length;
+ attachment->length = [self decodeAttachment: body encoding: encoding].length;
+ if (attachment->length == 0 && attachment->encodedLength > 0) {
+ *outStatus = kTDStatusBadEncoding; // failed to decode
return nil;
}
}
- *outStatus = [self insertAttachmentWithKey: [self keyForAttachment: body]
- forSequence: newRev.sequence
- named: filename
- type: contentType
- encoding: encoding
- length: body.length
- encodedLength: encodedLength
- revpos: newRev.generation];
- if (*outStatus >= 300)
+
+ if (![self storeBlob: body creatingKey: &attachment->blobKey]) {
+ *outStatus = kTDStatusAttachmentError;
+ return nil;
+ }
+
+ *outStatus = [self insertAttachment: attachment forSequence: newRev.sequence];
+ if (TDStatusIsError(*outStatus))
return nil;
}
- *outStatus = body ? 201 : 200;
+ *outStatus = body ? kTDStatusCreated : kTDStatusOK;
return newRev;
} @finally {
[self endTransaction: (*outStatus < 300)];
@@ -493,7 +591,7 @@ - (TDStatus) garbageCollectAttachments {
// Now collect all remaining attachment IDs and tell the store to delete all but these:
FMResultSet* r = [_fmdb executeQuery: @"SELECT DISTINCT key FROM attachments"];
if (!r)
- return 500;
+ return kTDStatusDBError;
NSMutableSet* allKeys = [NSMutableSet set];
while ([r next]) {
[allKeys addObject: [r dataForColumnIndex: 0]];
@@ -501,9 +599,9 @@ - (TDStatus) garbageCollectAttachments {
[r close];
NSInteger numDeleted = [_attachments deleteBlobsExceptWithKeys: allKeys];
if (numDeleted < 0)
- return 500;
+ return kTDStatusAttachmentError;
Log(@"Deleted %d attachments", numDeleted);
- return 200;
+ return kTDStatusOK;
}
View
2  Source/TDDatabase+Insertion.h
@@ -24,7 +24,7 @@ typedef BOOL (^TDValidationBlock) (TDRevision* newRevision,
/** Stores a new (or initial) revision of a document. This is what's invoked by a PUT or POST. As with those, the previous revision ID must be supplied when necessary and the call will fail if it doesn't match.
@param revision The revision to add. If the docID is nil, a new UUID will be assigned. Its revID must be nil. It must have a JSON body.
@param prevRevID The ID of the revision to replace (same as the "?rev=" parameter to a PUT), or nil if this is a new document.
- @param allowConflict If NO, an error status 409 will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child.
+ @param allowConflict If NO, an error status kTDStatusConflict will be returned if the insertion would create a conflict, i.e. if the previous revision already has a child.
@param status On return, an HTTP status code indicating success or failure.
@return A new TDRevision with the docID, revID and sequence filled in (but no body). */
- (TDRevision*) putRevision: (TDRevision*)revision
View
211 Source/TDDatabase+Insertion.m
@@ -16,12 +16,22 @@
#import "TDDatabase+Insertion.h"
#import "TDDatabase+Attachments.h"
#import <TouchDB/TDRevision.h>
+#import "TDCanonicalJSON.h"
+#import "TDAttachment.h"
#import "TDInternal.h"
#import "TDMisc.h"
+#import "Test.h"
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
+#ifdef GNUSTEP
+#import <openssl/sha.h>
+#else
+#define COMMON_DIGEST_FOR_OPENSSL
+#import <CommonCrypto/CommonDigest.h>
+#endif
+
NSString* const TDDatabaseChangeNotification = @"TDDatabaseChange";
@@ -65,16 +75,49 @@ + (NSString*) generateDocumentID {
}
-/** Given an existing revision ID, generates an ID for the next revision. */
-- (NSString*) generateNextRevisionID: (NSString*)revID {
- // Revision IDs have a generation count, a hyphen, and a UUID.
+/** Given an existing revision ID, generates an ID for the next revision.
+ Returns nil if prevID is invalid. */
+- (NSString*) generateIDForRevision: (TDRevision*)rev
+ withJSON: (NSData*)json
+ attachments: (NSDictionary*)attachments
+ prevID: (NSString*) prevID
+{
+ // Revision IDs have a generation count, a hyphen, and a hex digest.
unsigned generation = 0;
- if (revID) {
- generation = [TDRevision generationFromRevID: revID];
+ if (prevID) {
+ generation = [TDRevision generationFromRevID: prevID];
if (generation == 0)
return nil;
}
- NSString* digest = TDCreateUUID(); //TODO: Generate canonical digest of body
+
+ // Generate a digest for this revision based on the previous revision ID, document JSON,
+ // and attachment digests. This doesn't need to be secure; we just need to ensure that this
+ // code consistently generates the same ID given equivalent revisions.
+ MD5_CTX ctx;
+ unsigned char digestBytes[MD5_DIGEST_LENGTH];
+ MD5_Init(&ctx);
+
+ NSData* prevIDUTF8 = [prevID dataUsingEncoding: NSUTF8StringEncoding];
+ NSUInteger length = prevIDUTF8.length;
+ if (length > 0xFF)
+ return nil;
+ uint8_t lengthByte = length & 0xFF;
+ MD5_Update(&ctx, &lengthByte, 1); // prefix with length byte
+ if (length > 0)
+ MD5_Update(&ctx, prevIDUTF8.bytes, length);
+
+ uint8_t deletedByte = rev.deleted != NO;
+ MD5_Update(&ctx, &deletedByte, 1);
+
+ for (NSString* attName in [attachments.allKeys sortedArrayUsingSelector: @selector(compare:)]) {
+ TDAttachment* attachment = [attachments objectForKey: attName];
+ MD5_Update(&ctx, &attachment->blobKey, sizeof(attachment->blobKey));
+ }
+
+ MD5_Update(&ctx, json.bytes, json.length);
+
+ MD5_Final(digestBytes, &ctx);
+ NSString* digest = TDHexFromBytes(digestBytes, sizeof(digestBytes));
return [NSString stringWithFormat: @"%u-%@", generation+1, digest];
}
@@ -88,15 +131,6 @@ - (SInt64) insertDocumentID: (NSString*)docID {
}
-/** Maps a document ID to a numeric ID (row # in 'docs'), creating a new row if needed. */
-- (SInt64) getOrInsertDocNumericID: (NSString*)docID {
- SInt64 docNumericID = [self getDocNumericID: docID];
- if (docNumericID == 0)
- docNumericID = [self insertDocumentID: docID];
- return docNumericID;
-}
-
-
/** Extracts the history of revision IDs (in reverse chronological order) from the _revisions key */
+ (NSArray*) parseCouchDBRevisionHistory: (NSDictionary*)docProperties {
NSDictionary* revisions = $castIf(NSDictionary,
@@ -121,7 +155,8 @@ - (NSData*) encodeDocumentJSON: (TDRevision*)rev {
static NSSet* sSpecialKeysToRemove, *sSpecialKeysToLeave;
if (!sSpecialKeysToRemove) {
sSpecialKeysToRemove = [[NSSet alloc] initWithObjects: @"_id", @"_rev", @"_attachments",
- @"_deleted", @"_revisions", @"_revs_info", @"_conflicts", @"_deleted_conflicts", nil];
+ @"_deleted", @"_revisions", @"_revs_info", @"_conflicts", @"_deleted_conflicts",
+ @"_local_seq", nil];
sSpecialKeysToLeave = [[NSSet alloc] initWithObjects:
@"_replication_id", @"_replication_state", @"_replication_state_time", nil];
}