mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
434 lines
13 KiB
Objective-C
434 lines
13 KiB
Objective-C
/*
|
|
* Copyright 2017 Google
|
|
*
|
|
* 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 "FUtilities.h"
|
|
#import "FAtomicNumber.h"
|
|
#import "FConstants.h"
|
|
#import "FStringUtilities.h"
|
|
#import <FirebaseCore/FIRLogger.h>
|
|
|
|
#define ARC4RANDOM_MAX 0x100000000
|
|
#define INTEGER_32_MIN (-2147483648)
|
|
#define INTEGER_32_MAX 2147483647
|
|
|
|
#pragma mark -
|
|
#pragma mark C functions
|
|
|
|
FIRLoggerService kFIRLoggerDatabase = @"[Firebase/Database]";
|
|
static FLogLevel logLevel = FLogLevelInfo; // Default log level is info
|
|
static NSMutableDictionary *options = nil;
|
|
|
|
BOOL FFIsLoggingEnabled(FLogLevel level) { return level >= logLevel; }
|
|
|
|
void firebaseJobsTroll(void) {
|
|
FFLog(@"I-RDB095001",
|
|
@"password super secret; JFK conspiracy; Hello there! Having fun "
|
|
@"digging through Firebase? We're always hiring! jobs@firebase.com");
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Private property and singleton specification
|
|
|
|
@interface FUtilities () {
|
|
}
|
|
|
|
@property(nonatomic, strong) FAtomicNumber *localUid;
|
|
|
|
+ (FUtilities *)singleton;
|
|
|
|
@end
|
|
|
|
@implementation FUtilities
|
|
|
|
@synthesize localUid;
|
|
|
|
- (id)init {
|
|
self = [super init];
|
|
if (self) {
|
|
self.localUid = [[FAtomicNumber alloc] init];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
// TODO: We really want to be able to set the log level
|
|
+ (void)setLoggingEnabled:(BOOL)enabled {
|
|
logLevel = enabled ? FLogLevelDebug : FLogLevelInfo;
|
|
}
|
|
|
|
+ (BOOL)getLoggingEnabled {
|
|
return logLevel == FLogLevelDebug;
|
|
}
|
|
|
|
+ (FUtilities *)singleton {
|
|
static dispatch_once_t pred = 0;
|
|
__strong static id _sharedObject = nil;
|
|
dispatch_once(&pred, ^{
|
|
_sharedObject = [[self alloc] init]; // or some other init method
|
|
});
|
|
return _sharedObject;
|
|
}
|
|
|
|
// Refactor as a category of NSString
|
|
+ (NSArray *)splitString:(NSString *)str intoMaxSize:(const unsigned int)size {
|
|
if (str.length <= size) {
|
|
return [NSArray arrayWithObject:str];
|
|
}
|
|
|
|
NSMutableArray *dataSegs = [[NSMutableArray alloc] init];
|
|
for (int c = 0; c < str.length; c += size) {
|
|
if (c + size > str.length) {
|
|
int rangeStart = c;
|
|
unsigned long rangeLength = size - ((c + size) - str.length);
|
|
[dataSegs
|
|
addObject:[str substringWithRange:NSMakeRange(rangeStart,
|
|
rangeLength)]];
|
|
} else {
|
|
int rangeStart = c;
|
|
int rangeLength = size;
|
|
[dataSegs
|
|
addObject:[str substringWithRange:NSMakeRange(rangeStart,
|
|
rangeLength)]];
|
|
}
|
|
}
|
|
return dataSegs;
|
|
}
|
|
|
|
+ (NSNumber *)LUIDGenerator {
|
|
FUtilities *f = [FUtilities singleton];
|
|
return [f.localUid getAndIncrement];
|
|
}
|
|
|
|
+ (NSString *)decodePath:(NSString *)pathString {
|
|
NSMutableArray *decodedPieces = [[NSMutableArray alloc] init];
|
|
NSArray *pieces = [pathString componentsSeparatedByString:@"/"];
|
|
for (NSString *piece in pieces) {
|
|
if (piece.length > 0) {
|
|
[decodedPieces addObject:[FStringUtilities urlDecoded:piece]];
|
|
}
|
|
}
|
|
return [NSString
|
|
stringWithFormat:@"/%@", [decodedPieces componentsJoinedByString:@"/"]];
|
|
}
|
|
|
|
+ (NSString *)extractPathFromUrlString:(NSString *)url {
|
|
NSString *path = url;
|
|
|
|
NSRange schemeIndex = [path rangeOfString:@"//"];
|
|
if (schemeIndex.location != NSNotFound) {
|
|
path = [path substringFromIndex:schemeIndex.location + 2];
|
|
}
|
|
|
|
NSUInteger pathIndex = [path rangeOfString:@"/"].location;
|
|
if (pathIndex != NSNotFound) {
|
|
path = [path substringFromIndex:pathIndex + 1];
|
|
} else {
|
|
path = @"";
|
|
}
|
|
|
|
NSUInteger queryParamIndex = [path rangeOfString:@"?"].location;
|
|
if (queryParamIndex != NSNotFound) {
|
|
path = [path substringToIndex:queryParamIndex];
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
+ (FParsedUrl *)parseUrl:(NSString *)url {
|
|
// For backwards compatibility, support URLs without schemes on iOS.
|
|
if (![url containsString:@"://"]) {
|
|
url = [@"http://" stringByAppendingString:url];
|
|
}
|
|
|
|
NSString *originalPathString = [self extractPathFromUrlString:url];
|
|
|
|
// Sanitize the database URL by removing the path component, which may
|
|
// contain invalid URL characters.
|
|
NSString *sanitizedUrlWithoutPath =
|
|
[url stringByReplacingOccurrencesOfString:originalPathString
|
|
withString:@""];
|
|
NSURLComponents *urlComponents =
|
|
[NSURLComponents componentsWithString:sanitizedUrlWithoutPath];
|
|
if (!urlComponents) {
|
|
[NSException raise:@"Failed to parse database URL"
|
|
format:@"Failed to parse database URL: %@", url];
|
|
}
|
|
|
|
NSString *host = [urlComponents.host lowercaseString];
|
|
NSString *namespace;
|
|
bool secure;
|
|
|
|
if (urlComponents.port != nil) {
|
|
secure = [urlComponents.scheme isEqualToString:@"https"] ||
|
|
[urlComponents.scheme isEqualToString:@"wss"];
|
|
host = [host stringByAppendingFormat:@":%@", urlComponents.port];
|
|
} else {
|
|
secure = YES;
|
|
};
|
|
|
|
NSArray *parts = [urlComponents.host componentsSeparatedByString:@"."];
|
|
if ([parts count] == 3) {
|
|
namespace = [parts[0] lowercaseString];
|
|
} else {
|
|
// Attempt to extract namespace from "ns" query param.
|
|
NSArray *queryItems = urlComponents.queryItems;
|
|
for (NSURLQueryItem *item in queryItems) {
|
|
if ([item.name isEqualToString:@"ns"]) {
|
|
namespace = item.value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!namespace) {
|
|
namespace = [parts[0] lowercaseString];
|
|
}
|
|
}
|
|
|
|
NSString *pathString = [self
|
|
decodePath:[NSString stringWithFormat:@"/%@", originalPathString]];
|
|
FPath *path = [[FPath alloc] initWith:pathString];
|
|
FRepoInfo *repoInfo = [[FRepoInfo alloc] initWithHost:host
|
|
isSecure:secure
|
|
withNamespace:namespace];
|
|
|
|
FFLog(@"I-RDB095002", @"---> Parsed (%@) to: (%@,%@); ns=(%@); path=(%@)",
|
|
url, [repoInfo description], [repoInfo connectionURL],
|
|
repoInfo.namespace, [path description]);
|
|
|
|
FParsedUrl *parsedUrl = [[FParsedUrl alloc] init];
|
|
parsedUrl.repoInfo = repoInfo;
|
|
parsedUrl.path = path;
|
|
|
|
return parsedUrl;
|
|
}
|
|
|
|
/*
|
|
case str: JString => priString + "string:" + str.s;
|
|
case bool: JBool => priString + "boolean:" + bool.value;
|
|
case double: JDouble => priString + "number:" + double.num;
|
|
case int: JInt => priString + "number:" + int.num;
|
|
case _ => {
|
|
error("Leaf node has value '" + data.value + "' of invalid type '" +
|
|
data.value.getClass.toString + "'");
|
|
"";
|
|
}
|
|
*/
|
|
|
|
+ (NSString *)getJavascriptType:(id)obj {
|
|
if ([obj isKindOfClass:[NSDictionary class]]) {
|
|
return kJavaScriptObject;
|
|
} else if ([obj isKindOfClass:[NSString class]]) {
|
|
return kJavaScriptString;
|
|
} else if ([obj isKindOfClass:[NSNumber class]]) {
|
|
// We used to just compare to @encode(BOOL) as suggested at
|
|
// http://stackoverflow.com/questions/2518761/get-type-of-nsnumber, but
|
|
// on arm64, @encode(BOOL) returns "B" instead of "c" even though
|
|
// objCType still returns 'c' (signed char). So check both.
|
|
if (strcmp([obj objCType], @encode(BOOL)) == 0 ||
|
|
strcmp([obj objCType], @encode(signed char)) == 0) {
|
|
return kJavaScriptBoolean;
|
|
} else {
|
|
return kJavaScriptNumber;
|
|
}
|
|
} else {
|
|
return kJavaScriptNull;
|
|
}
|
|
}
|
|
|
|
+ (NSError *)errorForStatus:(NSString *)status andReason:(NSString *)reason {
|
|
static dispatch_once_t pred = 0;
|
|
__strong static NSDictionary *errorMap = nil;
|
|
__strong static NSDictionary *errorCodes = nil;
|
|
dispatch_once(&pred, ^{
|
|
errorMap = @{
|
|
@"permission_denied" : @"Permission Denied",
|
|
@"unavailable" : @"Service is unavailable",
|
|
kFErrorWriteCanceled : @"Write cancelled by user"
|
|
};
|
|
errorCodes = @{
|
|
@"permission_denied" : @1,
|
|
@"unavailable" : @2,
|
|
kFErrorWriteCanceled : @3
|
|
};
|
|
});
|
|
|
|
if ([status isEqualToString:kFWPResponseForActionStatusOk]) {
|
|
return nil;
|
|
} else {
|
|
NSInteger code;
|
|
NSString *desc = nil;
|
|
if (reason) {
|
|
desc = reason;
|
|
} else if ([errorMap objectForKey:status] != nil) {
|
|
desc = [errorMap objectForKey:status];
|
|
} else {
|
|
desc = status;
|
|
}
|
|
|
|
if ([errorCodes objectForKey:status] != nil) {
|
|
NSNumber *num = [errorCodes objectForKey:status];
|
|
code = [num integerValue];
|
|
} else {
|
|
// XXX what to do here?
|
|
code = 9999;
|
|
}
|
|
|
|
return [[NSError alloc]
|
|
initWithDomain:kFErrorDomain
|
|
code:code
|
|
userInfo:@{NSLocalizedDescriptionKey : desc}];
|
|
}
|
|
}
|
|
|
|
+ (NSNumber *)intForString:(NSString *)string {
|
|
static NSCharacterSet *notDigits = nil;
|
|
if (!notDigits) {
|
|
notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
|
|
}
|
|
if ([string rangeOfCharacterFromSet:notDigits].length == 0) {
|
|
NSInteger num;
|
|
NSScanner *scanner = [NSScanner scannerWithString:string];
|
|
if ([scanner scanInteger:&num]) {
|
|
return [NSNumber numberWithInteger:num];
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
+ (NSString *)ieee754StringForNumber:(NSNumber *)val {
|
|
double d = [val doubleValue];
|
|
NSData *data = [NSData dataWithBytes:&d length:sizeof(double)];
|
|
NSMutableString *str = [[NSMutableString alloc] init];
|
|
const unsigned char *buffer = (const unsigned char *)[data bytes];
|
|
for (int i = 0; i < data.length; i++) {
|
|
unsigned char byte = buffer[7 - i];
|
|
[str appendFormat:@"%02x", byte];
|
|
}
|
|
return str;
|
|
}
|
|
|
|
static inline BOOL tryParseStringToInt(__unsafe_unretained NSString *str,
|
|
NSInteger *integer) {
|
|
// First do some cheap checks (NOTE: The below checks are significantly
|
|
// faster than an equivalent regex :-( ).
|
|
NSUInteger length = str.length;
|
|
if (length > 11 || length == 0) {
|
|
return NO;
|
|
}
|
|
long long value = 0;
|
|
BOOL negative = NO;
|
|
NSUInteger i = 0;
|
|
if ([str characterAtIndex:0] == '-') {
|
|
if (length == 1) {
|
|
return NO;
|
|
}
|
|
negative = YES;
|
|
i = 1;
|
|
}
|
|
for (; i < length; i++) {
|
|
unichar c = [str characterAtIndex:i];
|
|
// Must be a digit, or '-' if it's the first char.
|
|
if (c < '0' || c > '9') {
|
|
return NO;
|
|
} else {
|
|
int charValue = c - '0';
|
|
value = value * 10 + charValue;
|
|
}
|
|
}
|
|
|
|
value = (negative) ? -value : value;
|
|
|
|
if (value < INTEGER_32_MIN || value > INTEGER_32_MAX) {
|
|
return NO;
|
|
} else {
|
|
*integer = (NSInteger)value;
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
+ (NSString *)maxName {
|
|
static dispatch_once_t once;
|
|
static NSString *maxName;
|
|
dispatch_once(&once, ^{
|
|
maxName = [[NSString alloc] initWithFormat:@"[MAX_NAME]"];
|
|
});
|
|
return maxName;
|
|
}
|
|
|
|
+ (NSString *)minName {
|
|
static dispatch_once_t once;
|
|
static NSString *minName;
|
|
dispatch_once(&once, ^{
|
|
minName = [[NSString alloc] initWithFormat:@"[MIN_NAME]"];
|
|
});
|
|
return minName;
|
|
}
|
|
|
|
+ (NSComparisonResult)compareKey:(NSString *)a toKey:(NSString *)b {
|
|
if (a == b) {
|
|
return NSOrderedSame;
|
|
} else if (a == [FUtilities minName] || b == [FUtilities maxName]) {
|
|
return NSOrderedAscending;
|
|
} else if (b == [FUtilities minName] || a == [FUtilities maxName]) {
|
|
return NSOrderedDescending;
|
|
} else {
|
|
NSInteger aAsInt, bAsInt;
|
|
if (tryParseStringToInt(a, &aAsInt)) {
|
|
if (tryParseStringToInt(b, &bAsInt)) {
|
|
if (aAsInt > bAsInt) {
|
|
return NSOrderedDescending;
|
|
} else if (aAsInt < bAsInt) {
|
|
return NSOrderedAscending;
|
|
} else if (a.length > b.length) {
|
|
return NSOrderedDescending;
|
|
} else if (a.length < b.length) {
|
|
return NSOrderedAscending;
|
|
} else {
|
|
return NSOrderedSame;
|
|
}
|
|
} else {
|
|
return (NSComparisonResult)NSOrderedAscending;
|
|
}
|
|
} else if (tryParseStringToInt(b, &bAsInt)) {
|
|
return (NSComparisonResult)NSOrderedDescending;
|
|
} else {
|
|
// Perform literal character by character search to prevent a > b &&
|
|
// b > a issues. Note that calling -(NSString
|
|
// *)decomposedStringWithCanonicalMapping also works.
|
|
return [a compare:b options:NSLiteralSearch];
|
|
}
|
|
}
|
|
}
|
|
|
|
+ (NSComparator)keyComparator {
|
|
return ^NSComparisonResult(__unsafe_unretained NSString *a,
|
|
__unsafe_unretained NSString *b) {
|
|
return [FUtilities compareKey:a toKey:b];
|
|
};
|
|
}
|
|
|
|
+ (NSComparator)stringComparator {
|
|
return ^NSComparisonResult(__unsafe_unretained NSString *a,
|
|
__unsafe_unretained NSString *b) {
|
|
return [a compare:b];
|
|
};
|
|
}
|
|
|
|
+ (double)randomDouble {
|
|
return ((double)arc4random() / ARC4RANDOM_MAX);
|
|
}
|
|
|
|
@end
|