| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- /*
- * 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"
- #import "Firestore/Source/Model/FSTDocumentKey.h"
- #import "Firestore/Source/Util/FSTAssert.h"
- #import "Firestore/Source/Util/FSTClasses.h"
- #import "Firestore/Source/Util/FSTUsageValidation.h"
- 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<NSString *> *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<NSString *> *)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<NSString *> *)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<NSString *> *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<NSString *> *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<NSString *> *)segments {
- return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count];
- }
- + (instancetype)pathWithServerFormat:(NSString *)fieldPath {
- NSMutableArray<NSString *> *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;
- }
- @end
- @implementation FSTResourcePath
- + (instancetype)pathWithSegments:(NSArray<NSString *> *)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;
- }
- @end
- NS_ASSUME_NONNULL_END
|