/* * 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 "Firestore/Source/Model/FSTPath.h" #include #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::model::FieldPath; using firebase::firestore::model::ResourcePath; NS_ASSUME_NONNULL_BEGIN @interface FSTPath () /** An underlying array of which a subset of elements are the segments of the path. */ @property(strong, nonatomic) NSArray *segments; /** The index into the segments array of the first segment in this path. */ @property int offset; @end @implementation FSTPath /** * Designated initializer. * * @param segments The underlying array of segments for the path. * @param offset The starting index in the underlying array for the subarray to use. * @param length The length of the subarray to use. */ - (instancetype)initWithSegments:(NSArray *)segments offset:(int)offset length:(int)length { FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count); FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset, (int)segments.count - offset); if (self = [super init]) { _segments = segments; _offset = offset; _length = length; } return self; } - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[FSTPath class]]) { return NO; } FSTPath *path = object; return [self compare:path] == NSOrderedSame; } - (NSUInteger)hash { NSUInteger hash = 0; for (int i = 0; i < self.length; ++i) { hash += [self segmentAtIndex:i].hash; } return hash; } - (NSString *)description { return [self canonicalString]; } - (id)objectAtIndexedSubscript:(int)index { return [self segmentAtIndex:index]; } - (NSString *)segmentAtIndex:(int)index { FSTAssert(index < self.length, @"index %d out of range", index); return self.segments[self.offset + index]; } - (NSString *)firstSegment { FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path"); return [self segmentAtIndex:0]; } - (NSString *)lastSegment { FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path"); return [self segmentAtIndex:self.length - 1]; } - (NSComparisonResult)compare:(FSTPath *)other { int length = MIN(self.length, other.length); for (int i = 0; i < length; ++i) { NSString *left = [self segmentAtIndex:i]; NSString *right = [other segmentAtIndex:i]; NSComparisonResult result = [left compare:right]; if (result != NSOrderedSame) { return result; } } if (self.length < other.length) { return NSOrderedAscending; } if (self.length > other.length) { return NSOrderedDescending; } return NSOrderedSame; } - (instancetype)pathWithSegments:(NSArray *)segments offset:(int)offset length:(int)length { return [[[self class] alloc] initWithSegments:segments offset:offset length:length]; } - (instancetype)pathByAppendingSegment:(NSString *)segment { int newLength = self.length + 1; NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; for (int i = 0; i < self.length; ++i) { [segments addObject:self[i]]; } [segments addObject:segment]; return [self pathWithSegments:segments offset:0 length:newLength]; } - (instancetype)pathByAppendingPath:(FSTPath *)path { int newLength = self.length + path.length; NSMutableArray *segments = [NSMutableArray arrayWithCapacity:newLength]; for (int i = 0; i < self.length; ++i) { [segments addObject:self[i]]; } for (int i = 0; i < path.length; ++i) { [segments addObject:path[i]]; } return [self pathWithSegments:segments offset:0 length:newLength]; } - (BOOL)isEmpty { return self.length == 0; } - (instancetype)pathByRemovingFirstSegment { FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path"); return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1]; } - (instancetype)pathByRemovingFirstSegments:(int)count { FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count, self.length); return [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count]; } - (instancetype)pathByRemovingLastSegment { FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path"); return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1]; } - (BOOL)isPrefixOfPath:(FSTPath *)other { if (other.length < self.length) { return NO; } for (int i = 0; i < self.length; ++i) { if (![self[i] isEqual:other[i]]) { return NO; } } return YES; } /** Returns a standardized string representation of this path. */ - (NSString *)canonicalString { @throw FSTAbstractMethodException(); // NOLINT } @end @implementation FSTFieldPath + (instancetype)pathWithSegments:(NSArray *)segments { return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; } + (instancetype)pathWithServerFormat:(NSString *)fieldPath { NSMutableArray *segments = [NSMutableArray array]; // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it // allows non-identifier path components, even if they aren't escaped. Technically, this will // mangle paths with backticks in them used in v1alpha1, but that's fine. const char *source = [fieldPath UTF8String]; char *segment = (char *)malloc(strlen(source) + 1); char *segmentEnd = segment; // If we're inside '`' backticks, then we should ignore '.' dots. BOOL inBackticks = NO; char c; do { // Examine current character. This is legit even on zero-length strings because there's always // a null terminator. c = *source++; switch (c) { case '\0': // Falls through case '.': if (!inBackticks) { // Segment is complete *segmentEnd = '\0'; if (segment == segmentEnd) { FSTThrowInvalidArgument( @"Invalid field path (%@). Paths must not be empty, begin with " @"'.', end with '.', or contain '..'", fieldPath); } [segments addObject:[NSString stringWithUTF8String:segment]]; segmentEnd = segment; } else { // copy into the current segment *segmentEnd++ = c; } break; case '`': if (inBackticks) { inBackticks = NO; } else { inBackticks = YES; } break; case '\\': // advance to escaped character c = *source++; // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); // Fall through default: // copy into the current segment *segmentEnd++ = c; break; } } while (c); FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath); free(segment); return [FSTFieldPath pathWithSegments:segments]; } + (instancetype)keyFieldPath { static FSTFieldPath *keyFieldPath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]]; }); return keyFieldPath; } + (instancetype)emptyPath { static FSTFieldPath *emptyPath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ emptyPath = [FSTFieldPath pathWithSegments:@[]]; }); return emptyPath; } /** Return YES if the string could be used as a segment in a field path without escaping. */ + (BOOL)isValidIdentifier:(NSString *)segment { if (segment.length == 0) { return NO; } unichar first = [segment characterAtIndex:0]; if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) { return NO; } for (int i = 1; i < segment.length; i++) { unichar c = [segment characterAtIndex:i]; if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) { return NO; } } return YES; } - (BOOL)isKeyFieldPath { return [self isEqual:FSTFieldPath.keyFieldPath]; } - (NSString *)canonicalString { NSMutableString *result = [NSMutableString string]; for (int i = 0; i < self.length; i++) { if (i > 0) { [result appendString:@"."]; } NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"]; if (![FSTFieldPath isValidIdentifier:escaped]) { escaped = [NSString stringWithFormat:@"`%@`", escaped]; } [result appendString:escaped]; } return result; } + (instancetype)fieldPathWithCPPFieldPath:(const FieldPath &)fieldPath { NSMutableArray *segments = [NSMutableArray arrayWithCapacity:fieldPath.size()]; for (int i = 0; i < fieldPath.size(); i++) { segments[i] = util::WrapNSString(fieldPath[i]); } return [FSTFieldPath pathWithSegments:segments]; } - (FieldPath)toCPPFieldPath { std::vector segments(self.length); for (int i = 0; i < self.length; i++) { segments[i] = [[self segmentAtIndex:i] UTF8String]; } return FieldPath(segments.begin(), segments.end()); } @end @implementation FSTResourcePath + (instancetype)pathWithSegments:(NSArray *)segments { return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; } + (instancetype)pathWithString:(NSString *)resourcePath { // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) // and just passes them through raw (they exist for legacy reasons and should not be used // frequently). if ([resourcePath rangeOfString:@"//"].location != NSNotFound) { FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath); } NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy]; // We may still have an empty segment at the beginning or end if they had a leading or trailing // slash (which we allow). [segments removeObject:@""]; return [self pathWithSegments:segments]; } - (NSString *)canonicalString { // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) // and just passes them through raw (they exist for legacy reasons and should not be used // frequently). NSMutableString *result = [NSMutableString string]; for (int i = 0; i < self.length; i++) { if (i > 0) { [result appendString:@"/"]; } [result appendString:self[i]]; } return result; } + (instancetype)resourcePathWithCPPResourcePath:(const ResourcePath &)resourcePath { NSMutableArray *segments = [NSMutableArray arrayWithCapacity:resourcePath.size()]; for (int i = 0; i < resourcePath.size(); i++) { segments[i] = util::WrapNSString(resourcePath[i]); } return [FSTResourcePath pathWithSegments:segments]; } - (ResourcePath)toCPPResourcePath { std::vector segments(self.length); for (int i = 0; i < self.length; i++) { segments[i] = [[self segmentAtIndex:i] UTF8String]; } return ResourcePath(segments.begin(), segments.end()); } @end NS_ASSUME_NONNULL_END