ably-cocoa/Source/ARTDataEncoder.m

245 lines
10 KiB
Objective-C

#import "ARTCrypto+Private.h"
#import "ARTLog.h"
#import "ARTDataEncoder.h"
#import "ARTDeltaCodec.h"
@implementation ARTDataEncoderOutput
- (id)initWithData:(id)data encoding:(NSString *)encoding errorInfo:(ARTErrorInfo *)errorInfo {
self = [super init];
if (self) {
_data = data;
_encoding = encoding;
_errorInfo = errorInfo;
}
return self;
}
@end
@implementation ARTDataEncoder {
id<ARTChannelCipher> _cipher;
ARTDeltaCodec *_deltaCodec;
NSString *_baseId;
}
- (instancetype)initWithCipherParams:(ARTCipherParams *)params error:(NSError **)error {
self = [super init];
if (self) {
if (params) {
_cipher = [ARTCrypto cipherWithParams:params];
if (!_cipher) {
if (error) {
NSString *desc = [NSString stringWithFormat:@"ARTDataEncoder failed to create cipher with name %@", params.algorithm];
*error = [NSError errorWithDomain:ARTAblyErrorDomain
code:0
userInfo:@{NSLocalizedDescriptionKey: desc}];
}
return nil;
}
}
_deltaCodec = [[ARTDeltaCodec alloc] init];
}
return self;
}
- (void)setDeltaCodecBase:(nullable id)data identifier:(NSString *)identifier {
_baseId = identifier;
if ([data isKindOfClass:[NSData class]]) {
[_deltaCodec setBase:data withId:identifier];
}
else if ([data isKindOfClass:[NSString class]]) {
[_deltaCodec setBase:[data dataUsingEncoding:NSUTF8StringEncoding] withId:identifier];
}
}
- (ARTDataEncoderOutput *)encode:(id)data {
NSString *encoding = nil;
id encoded = nil;
NSData *toBase64 = nil;
if (!data) {
return [[ARTDataEncoderOutput alloc] initWithData:data encoding:nil errorInfo:nil];
}
NSData *jsonEncoded = nil;
if ([data isKindOfClass:[NSArray class]] || [data isKindOfClass:[NSDictionary class]]) {
NSError *error = nil;
// Just check the error; we don't want to actually JSON-encode this. It's more like "convert to JSON-compatible data".
// We will store the result, though, because if we're encrypting, then yes, we need to use the JSON-encoded
// data before encrypting.
NSJSONWritingOptions options;
if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) {
options = NSJSONWritingSortedKeys;
}
else {
options = 0;
}
jsonEncoded = [NSJSONSerialization dataWithJSONObject:data options:options error:&error];
if (error) {
return [[ARTDataEncoderOutput alloc] initWithData:data encoding:nil errorInfo:[ARTErrorInfo createFromNSError:error]];
}
encoded = data;
encoding = @"json";
} else if ([data isKindOfClass:[NSString class]]) {
encoding = @"";
encoded = data;
} else if ([data isKindOfClass:[NSData class]]) {
encoded = data;
toBase64 = data;
}
if (_cipher) {
if ([encoded isKindOfClass:[NSArray class]] || [encoded isKindOfClass:[NSDictionary class]]) {
encoded = jsonEncoded;
encoding = [NSString artAddEncoding:@"utf-8" toString:encoding];
} else if ([encoded isKindOfClass:[NSString class]]) {
encoded = [data dataUsingEncoding:NSUTF8StringEncoding];
encoding = [NSString artAddEncoding:@"utf-8" toString:encoding];
}
ARTStatus *status = [_cipher encrypt:encoded output:&toBase64];
if (status.state != ARTStateOk) {
ARTErrorInfo *errorInfo = status.errorInfo ? status.errorInfo : [ARTErrorInfo createWithCode:0 message:@"encrypt failed"];
return [[ARTDataEncoderOutput alloc] initWithData:encoded encoding:encoding errorInfo:errorInfo];
}
encoding = [NSString artAddEncoding:[self cipherEncoding] toString:encoding];
} else if (jsonEncoded) {
encoded = [[NSString alloc] initWithData:jsonEncoded encoding:NSUTF8StringEncoding];
}
if (toBase64 != nil) {
encoded = [[toBase64 base64EncodedStringWithOptions:0] dataUsingEncoding:NSUTF8StringEncoding];
if (!encoded) {
return [[ARTDataEncoderOutput alloc] initWithData:toBase64 encoding:encoding errorInfo:[ARTErrorInfo createWithCode:0 message:@"base64 failed"]];
}
encoded = [[NSString alloc] initWithData:encoded encoding:NSUTF8StringEncoding];
encoding = [NSString artAddEncoding:@"base64" toString:encoding];
}
if (encoded == nil) {
return [[ARTDataEncoderOutput alloc] initWithData:data encoding:nil errorInfo:[ARTErrorInfo createWithCode:0 message:@"must be NSString, NSData, NSArray or NSDictionary."]];
}
return [[ARTDataEncoderOutput alloc] initWithData:encoded
encoding:encoding
errorInfo:nil];
}
- (ARTDataEncoderOutput *)decode:(id)data encoding:(NSString *)encoding {
return [self decode:data identifier:@"" encoding:encoding];
}
- (ARTDataEncoderOutput *)decode:(id)data identifier:(NSString *)identifier encoding:(NSString *)encoding {
if (!data || !encoding ) {
[self setDeltaCodecBase:data identifier:identifier];
return [[ARTDataEncoderOutput alloc] initWithData:data encoding:encoding errorInfo:nil];
}
ARTErrorInfo *errorInfo = nil;
NSArray *encodings = [encoding componentsSeparatedByString:@"/"];
NSString *outputEncoding = [NSString stringWithString:encoding];
for (NSUInteger i = [encodings count]; i > 0; i--) {
errorInfo = nil;
NSString *encoding = [encodings objectAtIndex:i-1];
if ([encoding isEqualToString:@"base64"]) {
if ([data isKindOfClass:[NSData class]]) { // E. g. when decrypted.
data = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
if ([data isKindOfClass:[NSString class]]) {
data = [[NSData alloc] initWithBase64EncodedString:(NSString *)data options:0];
} else {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorInvalidMessageDataOrEncoding
message:[NSString stringWithFormat:@"invalid data type for 'base64' decoding: '%@'", [data class]]];
}
} else if ([encoding isEqualToString:@""] || [encoding isEqualToString:@"utf-8"]) {
if ([data isKindOfClass:[NSData class]]) { // E. g. when decrypted.
data = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
if (![data isKindOfClass:[NSString class]]) {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorInvalidMessageDataOrEncoding
message:[NSString stringWithFormat:@"invalid data type for '%@' decoding: '%@'", encoding, [data class]]];
}
} else if ([encoding isEqualToString:@"json"]) {
if ([data isKindOfClass:[NSData class]]) { // E. g. when decrypted.
data = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
if ([data isKindOfClass:[NSString class]]) {
NSData *jsonData = [data dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
data = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error != nil) {
errorInfo = [ARTErrorInfo createFromNSError:error];
}
} else if (![data isKindOfClass:[NSArray class]] && ![data isKindOfClass:[NSDictionary class]]) {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorInvalidMessageDataOrEncoding
message:[NSString stringWithFormat:@"invalid data type for 'json' decoding: '%@'", [data class]]];
}
} else if (_cipher && [encoding isEqualToString:[self cipherEncoding]] && [data isKindOfClass:[NSData class]]) {
ARTStatus *status = [_cipher decrypt:data output:&data];
if (status.state != ARTStateOk) {
errorInfo = status.errorInfo ? status.errorInfo : [ARTErrorInfo createWithCode:ARTErrorInvalidMessageDataOrEncoding message:@"decrypt failed"];
}
} else if ([encoding isEqualToString:@"vcdiff"] && _deltaCodec) {
NSError *decodeError;
data = [_deltaCodec applyDelta:data deltaId:identifier baseId:_baseId error:&decodeError];
if (decodeError) {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:decodeError.localizedDescription];
}
else if (!data) {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:@"Data is nil"];
}
} else {
errorInfo = [ARTErrorInfo createWithCode:ARTErrorInvalidMessageDataOrEncoding
message:[NSString stringWithFormat:@"unknown encoding: '%@'", encoding]];
}
[self setDeltaCodecBase:data identifier:identifier];
if (errorInfo == nil) {
outputEncoding = [outputEncoding artRemoveLastEncoding];
} else {
break;
}
}
return [[ARTDataEncoderOutput alloc] initWithData:data
encoding:outputEncoding
errorInfo:errorInfo];
}
- (NSString *)cipherEncoding {
size_t keyLen = [_cipher keyLength];
if (keyLen == 128) {
return @"cipher+aes-128-cbc";
} else if (keyLen == 256) {
return @"cipher+aes-256-cbc";
}
return nil;
}
@end
@implementation NSString (ARTPayload)
+ (NSString *)artAddEncoding:(NSString *)encoding toString:(NSString *)s {
return [(s ? s : @"") stringByAppendingPathComponent:encoding];
}
- (NSString *)artLastEncoding {
return [self lastPathComponent];
}
- (NSString *)artRemoveLastEncoding {
NSString *encoding = [self stringByDeletingLastPathComponent];
if ([encoding length] == 0) {
return nil;
}
return encoding;
}
@end