From 0576a4c3e57f301b965ac5cc0c89a17122383d34 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 4 May 2026 12:58:46 +0900 Subject: [PATCH 1/5] Properly error check modification date if the file was deleted --- .../Categories/NSURL+TOFileSystemAttributes.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.m b/TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.m index 5430f9b..28ea91c 100644 --- a/TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.m +++ b/TOFileSystemObserver/Categories/NSURL+TOFileSystemAttributes.m @@ -30,7 +30,9 @@ - (BOOL)to_isCopying { // When files are still being copied, their // modification date is equal to the current device time. - return [self.to_modificationDate timeIntervalSinceDate:[NSDate date]] + NSDate *modificationDate = self.to_modificationDate; + if (modificationDate == nil) { return NO; } + return [modificationDate timeIntervalSinceDate:[NSDate date]] > (-kTOFileSystemObserverCopyingTimeDelay - FLT_EPSILON); } From 1f1ba953a6e4a32311a74e2ba59a3f6f9ebf0af3 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 4 May 2026 12:59:02 +0900 Subject: [PATCH 2/5] Expose whether files were correctly written or not --- .../Categories/NSURL+TOFileSystemUUID.h | 8 +++---- .../Categories/NSURL+TOFileSystemUUID.m | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.h b/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.h index 8c2f53f..17372e7 100644 --- a/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.h +++ b/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.h @@ -40,11 +40,11 @@ NS_ASSUME_NONNULL_BEGIN /** Returns the unique UUID value assigned to this file. */ - (nullable NSString *)to_fileSystemUUID; -/** Sets a predetermined UUID to be the value of the file. */ -- (void)to_setFileSystemUUID:(NSString *)uuid; +/** Sets a predetermined UUID to be the value of the file. Returns YES if the attribute was written. */ +- (BOOL)to_setFileSystemUUID:(NSString *)uuid; -/** Regardless if one exists, generate and save a new UUID. */ -- (NSString *)to_generateFileSystemUUID; +/** Regardless if one exists, generate and save a new UUID. Returns nil if the write failed. */ +- (nullable NSString *)to_generateFileSystemUUID; @end diff --git a/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.m b/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.m index d793d6e..66a7f14 100644 --- a/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.m +++ b/TOFileSystemObserver/Categories/NSURL+TOFileSystemUUID.m @@ -40,11 +40,14 @@ - (NSString *)to_fileSystemUUID // Allocate a buffer for the value (UUID values are always 36 characters) char value[36]; - // Fetch the value from disk - getxattr(filePath, keyName, value, 36, 0, 0); + // Fetch the value from disk. A short read means there's no valid UUID stored. + ssize_t bytesRead = getxattr(filePath, keyName, value, sizeof(value), 0, 0); + if (bytesRead != (ssize_t)sizeof(value)) { + return nil; + } // Convert to a string, and return if successful - NSString *uuid = [[NSString alloc] initWithBytes:value length:36 encoding:NSUTF8StringEncoding]; + NSString *uuid = [[NSString alloc] initWithBytes:value length:bytesRead encoding:NSUTF8StringEncoding]; if (uuid.length == 0) { return nil; } @@ -64,9 +67,10 @@ - (NSString *)to_fileSystemUUID return uuid; } -- (void)to_setFileSystemUUID:(NSString *)uuid +- (BOOL)to_setFileSystemUUID:(NSString *)uuid { - if (uuid.length > 0 && uuid.length != 36) { + if (uuid.length == 0) { return NO; } + if (uuid.length != 36) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"UUID must be 36 characters long!" userInfo:nil]; @@ -78,15 +82,16 @@ - (void)to_setFileSystemUUID:(NSString *)uuid // Convert the string to a C byte string const char *uuidString = [uuid cStringUsingEncoding:NSUTF8StringEncoding]; + if (uuidString == NULL) { return NO; } - // Save it to this file - setxattr(filePath, keyName, uuidString, strlen(uuidString), 0, 0); + // Save it to this file. UUID strings are always 36 ASCII bytes. + return setxattr(filePath, keyName, uuidString, 36, 0, 0) == 0; } - (NSString *)to_generateFileSystemUUID { NSString *uuid = [NSUUID UUID].UUIDString; - [self to_setFileSystemUUID:uuid]; + if (![self to_setFileSystemUUID:uuid]) { return nil; } return uuid; } From f4eb8a340e1736c81e506877057bace2ce0ce700 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 4 May 2026 13:08:02 +0900 Subject: [PATCH 3/5] Snapshot the hash table to avoid in-flight mutations --- .../Entities/Items/TOFileSystemItemList.m | 10 +++++----- TOFileSystemObserver/TOFileSystemObserver.m | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m b/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m index 6d83cdd..c13f85d 100644 --- a/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m +++ b/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m @@ -131,7 +131,7 @@ - (void)rebuildItemListForListingOrder } // Trigger the notification blocks to update any UI with this new order - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemItemListCallBlock(token.notificationBlock, self, changes); } } @@ -242,7 +242,7 @@ - (void)addItemWithUUID:(NSString *)uuid itemURL:(NSURL *)url // Perform the broadcast to any observing objects that this update ocurred TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; [changes addInsertionIndex:sortedIndex]; - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemItemListCallBlock(token.notificationBlock, self, changes); } } @@ -267,7 +267,7 @@ - (void)removeItemWithUUID:(NSString *)uuid fileURL:(NSURL *)url TOFileSystemItemListChanges *changes = [[TOFileSystemItemListChanges alloc] init]; [changes addDeletionIndex:index]; - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemItemListCallBlock(token.notificationBlock, self, changes); } } @@ -300,7 +300,7 @@ - (void)itemDidRefreshWithUUID:(NSString *)uuid [changes addModificationIndex:newIndex]; // Broadcast the changes - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemItemListCallBlock(token.notificationBlock, self, changes); } } @@ -331,7 +331,7 @@ - (void)synchronizeWithDisk // Broadcast the changes dispatch_async(dispatch_get_main_queue(), ^{ - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemItemListCallBlock(token.notificationBlock, self, changes); } }); diff --git a/TOFileSystemObserver/TOFileSystemObserver.m b/TOFileSystemObserver/TOFileSystemObserver.m index 3ffa3ac..4866a98 100644 --- a/TOFileSystemObserver/TOFileSystemObserver.m +++ b/TOFileSystemObserver/TOFileSystemObserver.m @@ -607,7 +607,7 @@ - (void)scanOperationWillBeginFullScan:(TOFileSystemScanOperation *)scanOperatio } // Inform all notification tokens registered - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemObserverCallBlock(token.notificationBlock, self, TOFileSystemObserverNotificationTypeWillBeginFullScan, @@ -631,7 +631,7 @@ - (void)scanOperationDidCompleteFullScan:(TOFileSystemScanOperation *)scanOperat } // Inform all notification tokens registered - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemObserverCallBlock(token.notificationBlock, self, TOFileSystemObserverNotificationTypeDidCompleteFullScan, @@ -667,7 +667,7 @@ - (void)postNotificationsWithChanges:(TOFileSystemChanges *)changes } // Inform all notification tokens registered - for (TOFileSystemNotificationToken *token in self.notificationTokens) { + for (TOFileSystemNotificationToken *token in self.notificationTokens.allObjects) { TOFileSystemObserverCallBlock(token.notificationBlock, self, TOFileSystemObserverNotificationTypeDidChange, From 8400467ee3e3f23c59065f2460724206459aabe2 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 4 May 2026 13:22:18 +0900 Subject: [PATCH 4/5] Add snapshot mechanism to prevent mutations during loops --- .../Entities/Collections/TOFileSystemItemMapTable.h | 3 +++ .../Entities/Collections/TOFileSystemItemMapTable.m | 11 +++++++++++ .../Entities/Items/TOFileSystemItemList.m | 5 +++-- TOFileSystemObserver/TOFileSystemObserver.m | 4 ++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h index 5a07f75..cccd4f8 100644 --- a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h +++ b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h @@ -37,6 +37,9 @@ NS_ASSUME_NONNULL_BEGIN - (id)itemForUUID:(NSString *)uuid; - (void)removeItemForUUID:(NSString *)uuid; +/** A point-in-time snapshot of all current items. Safe to iterate while the table is being mutated. */ +- (NSArray *)allItems; + /** Implementations for allowing dictionary style literal syntax. */ - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key; - (nullable id)objectForKeyedSubscript:(NSString *)key; diff --git a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m index 8e452ef..f72e421 100644 --- a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m +++ b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m @@ -81,6 +81,17 @@ - (void)removeItemForUUID:(NSString *)uuid }); } +- (NSArray *)allItems +{ + __block NSArray *items = nil; + dispatch_sync(self.dispatchQueue, ^{ + @autoreleasepool { + items = self.mapTable.objectEnumerator.allObjects; + } + }); + return items ?: @[]; +} + - (void)setObject:(nullable id)object forKeyedSubscript:(nonnull NSString *)key { [self setItem:object forUUID:key]; diff --git a/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m b/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m index c13f85d..9586f57 100644 --- a/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m +++ b/TOFileSystemObserver/Entities/Items/TOFileSystemItemList.m @@ -322,8 +322,9 @@ - (void)synchronizeWithDisk // Skip if every file was accounted for if (changes.deletions.count == 0) { return; } - // Remove all of the deleted files from the list - for (NSNumber *deletedIndex in changes.deletions) { + // Remove all of the deleted files from the list. Iterate from highest index + // to lowest so each removal doesn't shift the indices we still need to use. + for (NSNumber *deletedIndex in [changes.deletions reverseObjectEnumerator]) { NSString *uuid = self.sortedItems[deletedIndex.intValue]; [self.sortedItems removeObjectAtIndex:deletedIndex.intValue]; [self.items removeObjectForKey:uuid]; diff --git a/TOFileSystemObserver/TOFileSystemObserver.m b/TOFileSystemObserver/TOFileSystemObserver.m index 4866a98..e78881e 100644 --- a/TOFileSystemObserver/TOFileSystemObserver.m +++ b/TOFileSystemObserver/TOFileSystemObserver.m @@ -618,8 +618,8 @@ - (void)scanOperationWillBeginFullScan:(TOFileSystemScanOperation *)scanOperatio - (void)scanOperationDidCompleteFullScan:(TOFileSystemScanOperation *)scanOperation { // Loop through the list one more time to remove any headless entries - for (NSString *listUUID in self.itemListTable) { - [self.itemListTable[listUUID] synchronizeWithDisk]; + for (TOFileSystemItemList *list in self.itemListTable.allItems) { + [list synchronizeWithDisk]; } // Perform the Notification Center broadcast From cc40e50e1543d3d93b169564cd4a5c7c103fdbbc Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 4 May 2026 13:23:36 +0900 Subject: [PATCH 5/5] Drop the NSFastEnumeration protocol conformance --- .../Entities/Collections/TOFileSystemItemMapTable.h | 2 +- .../Entities/Collections/TOFileSystemItemMapTable.m | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h index cccd4f8..777aba7 100644 --- a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h +++ b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN used to store re-usable instances of item and list objects. */ -@interface TOFileSystemItemMapTable : NSObject +@interface TOFileSystemItemMapTable : NSObject @property (nonatomic, readonly) NSInteger count; diff --git a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m index f72e421..030df51 100644 --- a/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m +++ b/TOFileSystemObserver/Entities/Collections/TOFileSystemItemMapTable.m @@ -102,13 +102,4 @@ - (nullable id)objectForKeyedSubscript:(NSString *)key return [self itemForUUID:key]; } -- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state - objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer - count:(NSUInteger)len -{ - return [_mapTable countByEnumeratingWithState:state - objects:buffer - count:len]; -} - @end