From 05f793b6ef81830cb1f1f0afb296d4b684ddce9e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:03:17 -0500 Subject: [PATCH 01/46] Fix use-after-free in LibAV frame resize buffer management --- CoreZen/Media/LibAV/LibAVRenderController.m | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CoreZen/Media/LibAV/LibAVRenderController.m b/CoreZen/Media/LibAV/LibAVRenderController.m index 10181c5..b6c6a41 100644 --- a/CoreZen/Media/LibAV/LibAVRenderController.m +++ b/CoreZen/Media/LibAV/LibAVRenderController.m @@ -169,13 +169,7 @@ - (BOOL)resizeRawFrame:(AVFrame **)frame rgbFrame->height = scaledY; rgbFrame->format = AV_PIX_FMT_RGBA; - int bufferSize = av_image_get_buffer_size(rgbFrame->format, rgbFrame->width, rgbFrame->height, 1); - - uint8_t *rgbBuffer = av_malloc(bufferSize); - - int result = av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, rgbBuffer, rgbFrame->format, rgbFrame->width, rgbFrame->height, 1); - - av_free(rgbBuffer); + int result = av_frame_get_buffer(rgbFrame, 1); if (result >= 0) { // Cast to get around rules about adding `const` more than one level deep: https://stackoverflow.com/a/5055789 From 04ab2849052a34bef879512a2ef2fc94a19af0d8 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:04:55 -0500 Subject: [PATCH 02/46] Fix double initialization of MPVRenderController --- CoreZen/Media/Views/MediaPlayerView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Media/Views/MediaPlayerView.m b/CoreZen/Media/Views/MediaPlayerView.m index cfb3636..288245b 100644 --- a/CoreZen/Media/Views/MediaPlayerView.m +++ b/CoreZen/Media/Views/MediaPlayerView.m @@ -22,7 +22,7 @@ - (void)initCommon { ZENMPVViewLayer *viewLayer = [ZENMPVViewLayer new]; viewLayer.playerView = self; - _renderController = [[ZENMPVRenderController new] initWithPlayerView:self]; + _renderController = [[ZENMPVRenderController alloc] initWithPlayerView:self]; NSOpenGLPixelFormatAttribute pixelFormatAttrs[] = { NSOpenGLPFAAllowOfflineRenderers, From baac1911ab5481b25bc1c20040c8552ddc089d1d Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:05:45 -0500 Subject: [PATCH 03/46] Fix NSURL category methods using absoluteString instead of path --- CoreZen/Categories/NSURL+CoreZen.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CoreZen/Categories/NSURL+CoreZen.m b/CoreZen/Categories/NSURL+CoreZen.m index c9cb350..9de3ab2 100644 --- a/CoreZen/Categories/NSURL+CoreZen.m +++ b/CoreZen/Categories/NSURL+CoreZen.m @@ -55,8 +55,8 @@ + (BOOL)zen_volumeInfoForUUID:(NSUUID *)volumeUUID - (NSString *)zen_relativePathToURL:(NSURL *)url { - NSString *fullPath = [url absoluteString]; - NSString *basePath = [self absoluteString]; + NSString *fullPath = [url path]; + NSString *basePath = [self path]; if ([fullPath hasPrefix:basePath]) { NSString *relativePath = [fullPath substringFromIndex:basePath.length]; return relativePath; @@ -66,7 +66,7 @@ - (NSString *)zen_relativePathToURL:(NSURL *)url { - (NSUInteger)zen_fileSize { NSError *error; - NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:self.absoluteString error:&error]; + NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:self.path error:&error]; if (attributes) { return attributes.fileSize; } From bc30333931b1c1bbea5045c8c6cd30e63a660c13 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:06:44 -0500 Subject: [PATCH 04/46] Fix sumAllVideoDurationForUI: always returning 0 --- CoreZen/Domain/ObjectRepository.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Domain/ObjectRepository.m b/CoreZen/Domain/ObjectRepository.m index 7449a78..ed202e3 100644 --- a/CoreZen/Domain/ObjectRepository.m +++ b/CoreZen/Domain/ObjectRepository.m @@ -104,7 +104,7 @@ - (void)sumAllVideoDuration:(ZENAsyncCountCompletionBlock)countBlock { - (void)sumAllVideoDurationForUI:(ZENAsyncCountCompletionBlock)countBlock { // Bounce to main thread [self sumAllVideoDuration:^(NSUInteger count) { - ZENCallAsyncCountCompletionBlockOnMainThread(countBlock, 0); + ZENCallAsyncCountCompletionBlockOnMainThread(countBlock, count); }]; } From 78696837b1f6d3203c5cb76015fa85663c8d54b3 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:07:45 -0500 Subject: [PATCH 05/46] Fix asyncInitAndAdd:uiCompletion: dispatching to wrong thread --- CoreZen/Domain/ObjectRepository.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Domain/ObjectRepository.m b/CoreZen/Domain/ObjectRepository.m index ed202e3..bf595c9 100644 --- a/CoreZen/Domain/ObjectRepository.m +++ b/CoreZen/Domain/ObjectRepository.m @@ -224,7 +224,7 @@ - (void) asyncInitAndAdd:(ZENDomainObject *)domainObject - (void) asyncInitAndAdd:(ZENDomainObject *)domainObject uiCompletion:(ZENAsyncContinueBlock)completion { [self asyncInitAndAdd:domainObject completion:^{ - ZENCallAsyncContinueBlockOnThreadPool(completion); + ZENCallAsyncContinueBlockOnMainThread(completion); }]; } From 52830a16c020477b22df0c5bb47c9eda2cf52ae8 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:08:48 -0500 Subject: [PATCH 06/46] Fix crash when fetching non-existent object by identifier --- CoreZen/Domain/ObjectRepository.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CoreZen/Domain/ObjectRepository.m b/CoreZen/Domain/ObjectRepository.m index bf595c9..2ded216 100644 --- a/CoreZen/Domain/ObjectRepository.m +++ b/CoreZen/Domain/ObjectRepository.m @@ -134,6 +134,10 @@ - (void)fetchObjectByIdentifier:(ZENIdentifier)identifier // and we only ever create and cache objects on this queue. If there was a race, only one instance // can win and cache the object. The other instance would have a cache hit just before here. object = [self fetchObjectByIdentifier:identifier database:database]; + if (!object) { + ZENCallFetchResultsBlockOnThreadPool(resultsBlock, @[]); + return; + } [self.cache cacheObject:object]; // Async initialize object From 0da893fb482d79ac91a79ec2324111eab81d8550 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:11:31 -0500 Subject: [PATCH 07/46] Fix errant glClear after mpv render that blanked video frames --- CoreZen/Media/MPV/MPVRenderController.m | 1 - 1 file changed, 1 deletion(-) diff --git a/CoreZen/Media/MPV/MPVRenderController.m b/CoreZen/Media/MPV/MPVRenderController.m index cc5921c..dec2328 100644 --- a/CoreZen/Media/MPV/MPVRenderController.m +++ b/CoreZen/Media/MPV/MPVRenderController.m @@ -165,7 +165,6 @@ - (void)renderNextFrame { mpv_render_context_render(_mpvRenderContext, _mpvRenderParams); - glClear(GL_COLOR_BUFFER_BIT); glFlush(); [self.playerView unlockViewContext]; From 6e824863cb2e7b316257a60f1ae15488d12ca611 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:12:48 -0500 Subject: [PATCH 08/46] Fix missing KVO observer removal in media player views --- CoreZen/Media/Views/MediaPlayerCTIView.m | 12 ++++++++++++ CoreZen/Media/Views/MediaPlayerControlsView.m | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/CoreZen/Media/Views/MediaPlayerCTIView.m b/CoreZen/Media/Views/MediaPlayerCTIView.m index b39d3fc..ea08bb3 100644 --- a/CoreZen/Media/Views/MediaPlayerCTIView.m +++ b/CoreZen/Media/Views/MediaPlayerCTIView.m @@ -76,7 +76,19 @@ - (void)viewDidMoveToWindow { } } +- (void)removePlayerObservers { + if (self.player) { + [self.player removeObserver:self forKeyPath:@"positionPercent" context:ObserverContext]; + [self.player removeObserver:self forKeyPath:@"fileURL" context:ObserverContext]; + } +} + +- (void)dealloc { + [self removePlayerObservers]; +} + - (void)attachPlayer:(ZENMediaPlayer *)player { + [self removePlayerObservers]; self.player = player; if (player) { diff --git a/CoreZen/Media/Views/MediaPlayerControlsView.m b/CoreZen/Media/Views/MediaPlayerControlsView.m index 72f8903..b90f274 100644 --- a/CoreZen/Media/Views/MediaPlayerControlsView.m +++ b/CoreZen/Media/Views/MediaPlayerControlsView.m @@ -63,7 +63,18 @@ - (IBAction)buttonClicked:(id)sender { } } +- (void)removePlayerObservers { + if (self.player) { + [self.player removeObserver:self forKeyPath:@"paused" context:ObserverContext]; + } +} + +- (void)dealloc { + [self removePlayerObservers]; +} + - (void)attachPlayer:(ZENMediaPlayer *)player { + [self removePlayerObservers]; self.player = player; if (player) { From dd714818695355171154a9e4c52791a69aad569e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:14:14 -0500 Subject: [PATCH 09/46] Retain CGL context returned from copyCGLContextForPixelFormat: --- CoreZen/Media/MPV/MPVViewLayer.m | 1 + 1 file changed, 1 insertion(+) diff --git a/CoreZen/Media/MPV/MPVViewLayer.m b/CoreZen/Media/MPV/MPVViewLayer.m index 1047a80..401665d 100644 --- a/CoreZen/Media/MPV/MPVViewLayer.m +++ b/CoreZen/Media/MPV/MPVViewLayer.m @@ -52,6 +52,7 @@ - (CGLContextObj)copyCGLContextForPixelFormat:(CGLPixelFormatObj)pf { CGLSetParameter(context, kCGLCPSwapInterval, &sync); CGLSetCurrentContext(context); + CGLRetainContext(context); return context; } From 7589cebd9128339bd6e53d45d068ace62a97cd69 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:17:17 -0500 Subject: [PATCH 10/46] Fix retain cycle in CTI view frame render completion block --- CoreZen/Media/Views/MediaPlayerCTIView.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/Views/MediaPlayerCTIView.m b/CoreZen/Media/Views/MediaPlayerCTIView.m index ea08bb3..3b123c1 100644 --- a/CoreZen/Media/Views/MediaPlayerCTIView.m +++ b/CoreZen/Media/Views/MediaPlayerCTIView.m @@ -153,8 +153,9 @@ - (void)updateMediaFile:(NSURL *)url { NSUInteger width = 320; + __weak ZENMediaPlayerCTIView *weakSelf = self; [self.frameRenderer renderFrames:101 width:width height:width completion:^(NSArray *frames) { - self.previewFrames = frames; + weakSelf.previewFrames = frames; NSLog(@"Rendered %lu frames", frames.count); }]; From 8fdb273eb068c0c1ae251c21941869470cb80e5a Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:18:33 -0500 Subject: [PATCH 11/46] Fix ArchivedView nib lookup using wrong bundle for subclasses --- CoreZen/Views/ArchivedView.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Views/ArchivedView.m b/CoreZen/Views/ArchivedView.m index be3a6fc..77f8312 100644 --- a/CoreZen/Views/ArchivedView.m +++ b/CoreZen/Views/ArchivedView.m @@ -17,7 +17,7 @@ - (NSString *)archivedViewName { } - (void)initCommon { - NSBundle *bundle = [NSBundle bundleForClass:ZENArchivedView.class]; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSNib *nib = [[NSNib alloc] initWithNibNamed:self.archivedViewName bundle:bundle]; if ([nib instantiateWithOwner:self topLevelObjects:nil]) { NSView *nibView = self.rootView; From e91def52bce38289fe3991b75326c943ed782da5 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:19:49 -0500 Subject: [PATCH 12/46] Fix toolbar showing no selected item on first preferences show --- CoreZen/Preferences/PreferencesWindowController.m | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CoreZen/Preferences/PreferencesWindowController.m b/CoreZen/Preferences/PreferencesWindowController.m index 72fbd99..9be5ee6 100644 --- a/CoreZen/Preferences/PreferencesWindowController.m +++ b/CoreZen/Preferences/PreferencesWindowController.m @@ -113,6 +113,7 @@ - (void)activateViewController:(NSViewController *) NSWidth(frameRectForContentRect), NSHeight(frameRectForContentRect)); + self.activeViewController = viewController; self.window.title = viewController.preferenceDisplayName; NSView *contentView = viewController.view; @@ -124,9 +125,7 @@ - (void)activateViewController:(NSViewController *) context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; [self.window.animator setFrame:newWindowFrame display:YES]; [contentView.animator setAlphaValue:1.0]; - } completionHandler:^{ - self.activeViewController = viewController; - }]; + } completionHandler:nil]; } - (NSViewController *)viewControllerWithIdentifier:(NSString *)identifier { From c2b4beec6f44c80303d9c7217b8ab49a8789004b Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:21:02 -0500 Subject: [PATCH 13/46] Remove incorrect setFrameFromString: call in preferences window --- CoreZen/Preferences/PreferencesWindowController.m | 1 - 1 file changed, 1 deletion(-) diff --git a/CoreZen/Preferences/PreferencesWindowController.m b/CoreZen/Preferences/PreferencesWindowController.m index 9be5ee6..0a34635 100644 --- a/CoreZen/Preferences/PreferencesWindowController.m +++ b/CoreZen/Preferences/PreferencesWindowController.m @@ -58,7 +58,6 @@ - (instancetype)init { if (self) { [self center]; self.frameAutosaveName = @"ZENPreferencesWindow"; - [self setFrameFromString:@"ZENPreferencesWindow"]; } return self; } From c34b5dcf9a0b845fc614b046b4a2a00b56e4fb5b Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:21:59 -0500 Subject: [PATCH 14/46] Fix memory leak of mpv version property string --- CoreZen/Media/MPV/MPVPlayerController.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/MPV/MPVPlayerController.m b/CoreZen/Media/MPV/MPVPlayerController.m index 8aee32d..f805bdc 100644 --- a/CoreZen/Media/MPV/MPVPlayerController.m +++ b/CoreZen/Media/MPV/MPVPlayerController.m @@ -88,7 +88,9 @@ - (instancetype)initWithPlayer:(ZENMediaPlayer *)player { // Initialize MPV handle mpv_initialize(_mpvHandle); - _version = zen_mpv_to_nsstring(mpv_get_property_string(_mpvHandle, kMPVProperty_mpv_version)); + char *version = mpv_get_property_string(_mpvHandle, kMPVProperty_mpv_version); + _version = zen_mpv_to_nsstring(version); + mpv_free(version); // Disable subtitles zen_mpv_set_string_property(_mpvHandle, kMPVProperty_sid, kMPVPropertyKey_no); From 946216895b4069e7bb42f6eb4eefc1ef69eb2de5 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:22:49 -0500 Subject: [PATCH 15/46] Fix CFString leaks in OpenGL proc address lookup --- CoreZen/Media/MPV/MPVFunctions.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CoreZen/Media/MPV/MPVFunctions.m b/CoreZen/Media/MPV/MPVFunctions.m index d7b9300..60310e3 100644 --- a/CoreZen/Media/MPV/MPVFunctions.m +++ b/CoreZen/Media/MPV/MPVFunctions.m @@ -72,5 +72,7 @@ void zen_mpv_destroy_pthread_mutex_cond(pthread_mutex_t* mutex, pthread_cond_t* CFBundleRef bundle = CFBundleGetBundleWithIdentifier(bundleName); CFStringRef functionName = CFStringCreateWithCString(kCFAllocatorDefault, name, kCFStringEncodingASCII); void *function = CFBundleGetFunctionPointerForName(bundle, functionName); + CFRelease(functionName); + CFRelease(bundleName); return function; } From 637f576cc4eaaeb2edcd8e68e4182f63a161d470 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:23:23 -0500 Subject: [PATCH 16/46] Fix CGColor leak in peek view border color setup --- CoreZen/Media/Views/MediaPlayerPeekView.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/Views/MediaPlayerPeekView.m b/CoreZen/Media/Views/MediaPlayerPeekView.m index 54cc737..730013e 100644 --- a/CoreZen/Media/Views/MediaPlayerPeekView.m +++ b/CoreZen/Media/Views/MediaPlayerPeekView.m @@ -21,7 +21,9 @@ - (void)initCommon { self.layer.masksToBounds = YES; self.layer.shadowRadius = 2; self.layer.borderWidth = 1; - self.layer.borderColor = CGColorCreateGenericGray(0.7, 0.6); + CGColorRef borderColor = CGColorCreateGenericGray(0.7, 0.6); + self.layer.borderColor = borderColor; + CGColorRelease(borderColor); self.imageView.wantsLayer = YES; self.imageView.layer.cornerRadius = 4; From eaa03c1984d6a7522112d22ec7da1a6cdedcb67e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:24:13 -0500 Subject: [PATCH 17/46] Fix tableNames using static variable shared across all instances --- CoreZen/Database/DatabaseSchema.m | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/CoreZen/Database/DatabaseSchema.m b/CoreZen/Database/DatabaseSchema.m index 0083b12..58aeb68 100644 --- a/CoreZen/Database/DatabaseSchema.m +++ b/CoreZen/Database/DatabaseSchema.m @@ -51,12 +51,9 @@ - (void)initializeDatabase:(FMDatabase *)database { } - (NSArray *)tableNames { - static NSMutableArray *tableNames = nil; - if (!tableNames) { - tableNames = [NSMutableArray array]; - for (Class tableClass in self.tableClasses) { - [tableNames addObject:[tableClass tableName]]; - } + NSMutableArray *tableNames = [NSMutableArray array]; + for (Class tableClass in self.tableClasses) { + [tableNames addObject:[tableClass tableName]]; } return tableNames; } From e1a7146ca95b2ad7750be9b56a037b0e31a02ce0 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:24:42 -0500 Subject: [PATCH 18/46] Add NULL check in zen_mpv_to_nsstring --- CoreZen/Media/MPV/MPVFunctions.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CoreZen/Media/MPV/MPVFunctions.m b/CoreZen/Media/MPV/MPVFunctions.m index 60310e3..70c9a16 100644 --- a/CoreZen/Media/MPV/MPVFunctions.m +++ b/CoreZen/Media/MPV/MPVFunctions.m @@ -17,6 +17,9 @@ #pragma clang diagnostic pop NSString *zen_mpv_to_nsstring(const char *str) { + if (!str) { + return nil; + } return [NSString stringWithCString:str encoding:NSASCIIStringEncoding]; } From 362d140e075a00e1e9ac3001147fd06f0f3e7d88 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:25:09 -0500 Subject: [PATCH 19/46] Add NULL check after mpv_create failure --- CoreZen/Media/MPV/MPVPlayerController.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CoreZen/Media/MPV/MPVPlayerController.m b/CoreZen/Media/MPV/MPVPlayerController.m index f805bdc..01e9af6 100644 --- a/CoreZen/Media/MPV/MPVPlayerController.m +++ b/CoreZen/Media/MPV/MPVPlayerController.m @@ -76,6 +76,10 @@ - (instancetype)initWithPlayer:(ZENMediaPlayer *)player { // Create MPV handle (initialization happens after configuration) _mpvHandle = mpv_create(); + if (!_mpvHandle) { + NSLog(@"ERROR: mpv_create() failed"); + return nil; + } _clientName = zen_mpv_to_nsstring(mpv_client_name(_mpvHandle)); From b321aa4899184bb6ec27aea1e42721f92f79ebe0 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:25:55 -0500 Subject: [PATCH 20/46] Guard pthread_cond_wait against spurious wakeups in render loop --- CoreZen/Media/MPV/MPVRenderController.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/MPV/MPVRenderController.m b/CoreZen/Media/MPV/MPVRenderController.m index dec2328..e75400c 100644 --- a/CoreZen/Media/MPV/MPVRenderController.m +++ b/CoreZen/Media/MPV/MPVRenderController.m @@ -190,7 +190,9 @@ - (void)renderFrameOnRenderQueue { pthread_mutex_unlock(&self->_renderMutex); }); - pthread_cond_wait(&_renderCondition, &_renderMutex); + while (!_didRenderFrame && !_terminated) { + pthread_cond_wait(&_renderCondition, &_renderMutex); + } BOOL terminated = _terminated; BOOL didRenderFrame = _didRenderFrame; From 9dbefbdc5e22a274a5b359a9fe9cf029fd17e413 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:27:53 -0500 Subject: [PATCH 21/46] Fix NULL deref and signed-to-unsigned cast in durationMicroseconds --- CoreZen/Media/LibAV/LibAVInfoController.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/LibAV/LibAVInfoController.m b/CoreZen/Media/LibAV/LibAVInfoController.m index 6fa1b31..31ee6d9 100644 --- a/CoreZen/Media/LibAV/LibAVInfoController.m +++ b/CoreZen/Media/LibAV/LibAVInfoController.m @@ -136,7 +136,10 @@ - (ZENLibAVRenderController *)frameRenderController { - (NSUInteger)durationMicroseconds { NSAssert(AV_TIME_BASE == 1000000, @"Expected libav AV_TIME_BASE 1000000 for microseconds, instead = %d", AV_TIME_BASE); - return _formatContext->duration; + if (!_formatContext || _formatContext->duration < 0) { + return 0; + } + return (NSUInteger)_formatContext->duration; } - (double)durationSeconds { From 81df3f5157f4d08599ddbd4647cb67c833c4dc51 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:30:34 -0500 Subject: [PATCH 22/46] Fix breadth-first traversal producing incorrect order for deep trees --- CoreZen/Node/Node.m | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/CoreZen/Node/Node.m b/CoreZen/Node/Node.m index 137234a..0fb2f65 100644 --- a/CoreZen/Node/Node.m +++ b/CoreZen/Node/Node.m @@ -14,7 +14,6 @@ @interface ZENNode () @property (nonatomic) NSInteger size; - (void)enumerateDepthFirstHelper:(ZENNodeEnumerateBlock)block index:(NSUInteger *)index stop:(BOOL *)stop; -- (void)enumerateBreadthFirstHelper:(ZENNodeEnumerateBlock)block index:(NSUInteger *)index stop:(BOOL *)stop; - (instancetype)initZEN:(NSString *)name size:(NSInteger)size; @@ -211,30 +210,19 @@ - (void)enumerateDepthFirstUsingBlock:(ZENNodeEnumerateBlock)block { [self enumerateDepthFirstHelper:block index:&index stop:&stop]; } -- (void)enumerateBreadthFirstHelper:(ZENNodeEnumerateBlock)block - index:(NSUInteger *)index - stop:(BOOL *)stop { - for (ZENNode *node in self.children) { - block(node, *index, stop); - ++(*index); - if (*stop) { - break; - } - } - if (!*stop) { - for (ZENNode *node in self.children) { - [node enumerateBreadthFirstHelper:block index:index stop:stop]; - if (*stop) { - break; - } - } - } -} - - (void)enumerateBreadthFirstUsingBlock:(ZENNodeEnumerateBlock)block { NSUInteger index = 0; BOOL stop = NO; - [self enumerateBreadthFirstHelper:block index:&index stop:&stop]; + NSMutableArray *queue = [NSMutableArray arrayWithArray:self.children]; + while (queue.count > 0 && !stop) { + ZENNode *node = queue.firstObject; + [queue removeObjectAtIndex:0]; + block(node, index, &stop); + ++index; + if (!stop) { + [queue addObjectsFromArray:node.children]; + } + } } @end From 74cec7d764391b78e0bf734051022340d2eba7c1 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:31:06 -0500 Subject: [PATCH 23/46] Fix off-by-one in countChildrenAndDescendants --- CoreZen/Node/Node.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Node/Node.m b/CoreZen/Node/Node.m index 0fb2f65..d41c696 100644 --- a/CoreZen/Node/Node.m +++ b/CoreZen/Node/Node.m @@ -71,7 +71,7 @@ - (NSUInteger)countLeaves { - (NSUInteger)countChildrenAndDescendants { __block NSUInteger count = 0; [self enumerateDepthFirstUsingBlock:^(ZENNode *node, NSUInteger index, BOOL *stop) { - count = index; + ++count; }]; return count; } From 81958541a87ca332f015a48029db53e246441989 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:31:55 -0500 Subject: [PATCH 24/46] Fix MultiToken activate always returning YES --- CoreZen/Queue/WorkQueue.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Queue/WorkQueue.m b/CoreZen/Queue/WorkQueue.m index 0d8b7ff..6f7bef6 100644 --- a/CoreZen/Queue/WorkQueue.m +++ b/CoreZen/Queue/WorkQueue.m @@ -81,7 +81,7 @@ - (BOOL)activated { } - (BOOL)activate { - BOOL result = (self.tokens.count > 0); + BOOL result = NO; for (ZENWorkQueueToken *token in self.tokens.reverseObjectEnumerator) { result |= [token activate]; } From 9aea3d2f8d0368812479ac9bf462d19883946ffc Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:33:56 -0500 Subject: [PATCH 25/46] Handle mpv_render_context_create failure --- CoreZen/Media/MPV/MPVRenderController.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/MPV/MPVRenderController.m b/CoreZen/Media/MPV/MPVRenderController.m index e75400c..262393a 100644 --- a/CoreZen/Media/MPV/MPVRenderController.m +++ b/CoreZen/Media/MPV/MPVRenderController.m @@ -106,7 +106,12 @@ - (void)initRenderContext { [self.playerView lockViewContext]; - __unused int error = mpv_render_context_create(&_mpvRenderContext, self.mpvHandleFromPlayerView, renderParams); + int error = mpv_render_context_create(&_mpvRenderContext, self.mpvHandleFromPlayerView, renderParams); + if (error < 0) { + NSLog(@"ERROR: mpv_render_context_create failed: %d", error); + [self.playerView unlockViewContext]; + return; + } mpv_render_context_set_update_callback(_mpvRenderContext, zen_mpv_render_context_update, selfAsVoid); From 4f18c1d9ed68dec262691603f473af911f4a7f4b Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:37:08 -0500 Subject: [PATCH 26/46] Remove unsafe_unretained in mpv async dispatch callbacks --- CoreZen/Media/MPV/MPVPlayerController.m | 2 +- CoreZen/Media/MPV/MPVRenderController.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CoreZen/Media/MPV/MPVPlayerController.m b/CoreZen/Media/MPV/MPVPlayerController.m index 01e9af6..8498d9b 100644 --- a/CoreZen/Media/MPV/MPVPlayerController.m +++ b/CoreZen/Media/MPV/MPVPlayerController.m @@ -363,7 +363,7 @@ - (void)mpvHandleEvents { @end static void zen_mpv_wakeup(void *ctx) { - __unsafe_unretained ZENMPVPlayerController *controller = (__bridge ZENMPVPlayerController *)ctx; + ZENMPVPlayerController *controller = (__bridge ZENMPVPlayerController *)ctx; dispatch_async(controller->_eventQueue, ^{ [controller mpvHandleEvents]; }); diff --git a/CoreZen/Media/MPV/MPVRenderController.m b/CoreZen/Media/MPV/MPVRenderController.m index 262393a..81514df 100644 --- a/CoreZen/Media/MPV/MPVRenderController.m +++ b/CoreZen/Media/MPV/MPVRenderController.m @@ -214,7 +214,7 @@ - (void)renderFrameOnRenderQueue { @end static void zen_mpv_render_context_update(void *ctx) { - __unsafe_unretained ZENMPVRenderController *controller = (__bridge ZENMPVRenderController *)ctx; + ZENMPVRenderController *controller = (__bridge ZENMPVRenderController *)ctx; dispatch_async(controller->_renderQueue, ^{ [controller renderFrameOnRenderQueue]; }); From 9f353c79ba38df55a4a71d8f648a70be4786b100 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:43:42 -0500 Subject: [PATCH 27/46] Fix render loop deadlock when no frame is ready to draw --- CoreZen/Media/MPV/MPVRenderController.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CoreZen/Media/MPV/MPVRenderController.m b/CoreZen/Media/MPV/MPVRenderController.m index 81514df..4898cce 100644 --- a/CoreZen/Media/MPV/MPVRenderController.m +++ b/CoreZen/Media/MPV/MPVRenderController.m @@ -32,6 +32,7 @@ @interface ZENMPVRenderController () mpv_render_param _mpvSkipRenderParams[2]; BOOL _didRenderFrame; + BOOL _signaled; BOOL _terminated; pthread_mutex_t _renderMutex; @@ -186,16 +187,18 @@ - (void)renderFrameOnRenderQueue { pthread_mutex_lock(&_renderMutex); _didRenderFrame = NO; + _signaled = NO; dispatch_async(dispatch_get_main_queue(), ^{ [self.playerView.layer display]; pthread_mutex_lock(&self->_renderMutex); + self->_signaled = YES; pthread_cond_signal(&self->_renderCondition); pthread_mutex_unlock(&self->_renderMutex); }); - while (!_didRenderFrame && !_terminated) { + while (!_signaled && !_terminated) { pthread_cond_wait(&_renderCondition, &_renderMutex); } From fd4ce787fe17e59bbabb9cb4dde811395cd9bd42 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:44:24 -0500 Subject: [PATCH 28/46] Add NULL check for codec and stream in LibAV render init --- CoreZen/Media/LibAV/LibAVRenderController.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CoreZen/Media/LibAV/LibAVRenderController.m b/CoreZen/Media/LibAV/LibAVRenderController.m index b6c6a41..011596f 100644 --- a/CoreZen/Media/LibAV/LibAVRenderController.m +++ b/CoreZen/Media/LibAV/LibAVRenderController.m @@ -49,6 +49,10 @@ @implementation ZENLibAVRenderController - (void)avInitWithCodec:(const AVCodec *)codec stream:(const AVStream *)stream { + if (!codec || !stream) { + NSLog(@"avInitWithCodec: codec or stream is NULL"); + return; + } _codecContext = avcodec_alloc_context3(codec); avcodec_parameters_to_context(_codecContext, stream->codecpar); From 8107e902d685ad8bb348c89735d2e8d84a2ca9fa Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:45:24 -0500 Subject: [PATCH 29/46] Fix AVFrame leak in resizeRawFrame when source size is zero --- CoreZen/Media/LibAV/LibAVRenderController.m | 68 +++++++++++---------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/CoreZen/Media/LibAV/LibAVRenderController.m b/CoreZen/Media/LibAV/LibAVRenderController.m index 011596f..b8d79fa 100644 --- a/CoreZen/Media/LibAV/LibAVRenderController.m +++ b/CoreZen/Media/LibAV/LibAVRenderController.m @@ -160,49 +160,51 @@ - (BOOL)resizeRawFrame:(AVFrame **)frame NSSize sourceSize = NSMakeSize(sourceFrame->width, sourceFrame->height); - if (sourceSize.width > 0 && sourceSize.height > 0) { - - double factorX = maxSize.width / sourceSize.width; - double factorY = maxSize.height / sourceSize.height; - double scaleFactor = fmin(factorX, factorY); + if (sourceSize.width <= 0 || sourceSize.height <= 0) { + av_frame_free(&rgbFrame); + return NO; + } + + double factorX = maxSize.width / sourceSize.width; + double factorY = maxSize.height / sourceSize.height; + double scaleFactor = fmin(factorX, factorY); + + double scaledX = round(sourceSize.width * scaleFactor); + double scaledY = round(sourceSize.height * scaleFactor); + + rgbFrame->width = scaledX; + rgbFrame->height = scaledY; + rgbFrame->format = AV_PIX_FMT_RGBA; + + int result = av_frame_get_buffer(rgbFrame, 1); + + if (result >= 0) { + // Cast to get around rules about adding `const` more than one level deep: https://stackoverflow.com/a/5055789 + const uint8_t * const * frameData = (const uint8_t * const *)sourceFrame->data; - double scaledX = round(sourceSize.width * scaleFactor); - double scaledY = round(sourceSize.height * scaleFactor); + // Create scale context + struct SwsContext *scaleContext = sws_getContext(_codecContext->width, _codecContext->height, _codecContext->pix_fmt, rgbFrame->width, rgbFrame->height, rgbFrame->format, SWS_BILINEAR, NULL, NULL, NULL); - rgbFrame->width = scaledX; - rgbFrame->height = scaledY; - rgbFrame->format = AV_PIX_FMT_RGBA; + // Scale and convert colorspace to new frame + result = sws_scale(scaleContext, frameData, sourceFrame->linesize, 0, _codecContext->height, rgbFrame->data, rgbFrame->linesize); - int result = av_frame_get_buffer(rgbFrame, 1); + // Clean up scale context + sws_freeContext(scaleContext); if (result >= 0) { - // Cast to get around rules about adding `const` more than one level deep: https://stackoverflow.com/a/5055789 - const uint8_t * const * frameData = (const uint8_t * const *)sourceFrame->data; - - // Create scale context - struct SwsContext *scaleContext = sws_getContext(_codecContext->width, _codecContext->height, _codecContext->pix_fmt, rgbFrame->width, rgbFrame->height, rgbFrame->format, SWS_BILINEAR, NULL, NULL, NULL); - - // Scale and convert colorspace to new frame - result = sws_scale(scaleContext, frameData, sourceFrame->linesize, 0, _codecContext->height, rgbFrame->data, rgbFrame->linesize); - - // Clean up scale context - sws_freeContext(scaleContext); - - if (result >= 0) { - frameToFree = sourceFrame; - *frame = rgbFrame; - success = YES; + frameToFree = sourceFrame; + *frame = rgbFrame; + success = YES; - } else { - NSLog(@"sws_scale failed: %d", result); - } } else { - NSLog(@"av_image_fill_arrays failed: %d", result); + NSLog(@"sws_scale failed: %d", result); } - - av_frame_free(&frameToFree); + } else { + NSLog(@"av_frame_get_buffer failed: %d", result); } + av_frame_free(&frameToFree); + return success; } From 72ffeb88b0d5285298e4426dfbda18d62fb465f6 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:46:39 -0500 Subject: [PATCH 30/46] Fix setPreferenceViewControllers: appending instead of replacing --- CoreZen/Preferences/PreferencesWindowController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/CoreZen/Preferences/PreferencesWindowController.m b/CoreZen/Preferences/PreferencesWindowController.m index 0a34635..391b1e7 100644 --- a/CoreZen/Preferences/PreferencesWindowController.m +++ b/CoreZen/Preferences/PreferencesWindowController.m @@ -146,6 +146,7 @@ - (void)toolbarItemAction:(NSToolbarItem *)toolbarItem { #pragma mark - Public API - (void)setPreferenceViewControllers:(NSArray *)viewControllers { + [self.viewControllers removeAllObjects]; for (NSViewController *viewController in viewControllers) { NSAssert([viewController conformsToProtocol:@protocol(ZENPreferenceViewController)], @"ERROR: The viewController [%@] must conform to protocol ", [viewController class]); [self.viewControllers addObject:viewController]; From 9c3c93aab5ebcc5e2fa7b66de4ccfdf0b43a8334 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:47:15 -0500 Subject: [PATCH 31/46] Add NULL guard for outVolumeUUID in zen_volumeName --- CoreZen/Categories/NSURL+CoreZen.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CoreZen/Categories/NSURL+CoreZen.m b/CoreZen/Categories/NSURL+CoreZen.m index 9de3ab2..94e8803 100644 --- a/CoreZen/Categories/NSURL+CoreZen.m +++ b/CoreZen/Categories/NSURL+CoreZen.m @@ -17,7 +17,9 @@ - (BOOL)zen_volumeName:(NSString **)outVolumeName if ([self getResourceValue:outVolumeName forKey:NSURLVolumeLocalizedNameKey error:outError] && [self getResourceValue:outVolumeURL forKey:NSURLVolumeURLKey error:outError] && [self getResourceValue:&volumeUUID forKey:NSURLVolumeUUIDStringKey error:outError]) { - *outVolumeUUID = [[NSUUID alloc] initWithUUIDString:volumeUUID]; + if (outVolumeUUID) { + *outVolumeUUID = [[NSUUID alloc] initWithUUIDString:volumeUUID]; + } return YES; } return NO; From 010e12bbbb61978f1f89b3c93fa3c631f634fb21 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:47:58 -0500 Subject: [PATCH 32/46] Replace assert with NSCAssert in SecRandomCopyBytes error check --- CoreZen/Categories/NSNumber+CoreZen.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Categories/NSNumber+CoreZen.m b/CoreZen/Categories/NSNumber+CoreZen.m index 1cecfdc..d17752e 100644 --- a/CoreZen/Categories/NSNumber+CoreZen.m +++ b/CoreZen/Categories/NSNumber+CoreZen.m @@ -14,7 +14,7 @@ @implementation NSNumber (CoreZen) + (NSInteger)zen_randomInteger { int64_t randomBytes; int result = SecRandomCopyBytes(kSecRandomDefault, 8, &randomBytes); - assert(result == errSecSuccess); + NSCAssert(result == errSecSuccess, @"SecRandomCopyBytes failed with status %d", result); return randomBytes; } From 0dafe44aba1fe44d7034f64ef5de6c24928e0a71 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:48:46 -0500 Subject: [PATCH 33/46] Implement NSCopying on ZENNode to fix copyChildren crash --- CoreZen/Node/Node.h | 2 +- CoreZen/Node/Node.m | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CoreZen/Node/Node.h b/CoreZen/Node/Node.h index e1a8a41..663073b 100644 --- a/CoreZen/Node/Node.h +++ b/CoreZen/Node/Node.h @@ -10,7 +10,7 @@ @class ZENNode; typedef void (^ZENNodeEnumerateBlock)(id node, NSUInteger index, BOOL *stop); -@interface ZENNode : NSObject +@interface ZENNode : NSObject @property (nonatomic, readonly, strong) NSUUID *nodeID; diff --git a/CoreZen/Node/Node.m b/CoreZen/Node/Node.m index d41c696..748aae7 100644 --- a/CoreZen/Node/Node.m +++ b/CoreZen/Node/Node.m @@ -121,6 +121,12 @@ - (void)removeChildNode:(ZENNode *)child { } } +- (id)copyWithZone:(NSZone *)zone { + ZENNode *copy = [[self.class allocWithZone:zone] initWithName:self.name size:self.size]; + [copy copyChildren:self]; + return copy; +} + - (void)copyChildren:(ZENNode *)node { for (ZENNode *child in node.children) { [self addChildNode:[child copy]]; From a4741bad8894af58dee102eeafa1035b67f3e88f Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:51:18 -0500 Subject: [PATCH 34/46] Fix cache/DB inconsistency by moving cache ops inside transactions --- CoreZen/Domain/ObjectRepository.m | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/CoreZen/Domain/ObjectRepository.m b/CoreZen/Domain/ObjectRepository.m index 2ded216..427b4ac 100644 --- a/CoreZen/Domain/ObjectRepository.m +++ b/CoreZen/Domain/ObjectRepository.m @@ -202,10 +202,11 @@ - (void)addObject:(ZENDomainObject *)domainObject ZENObjectRepositoryNotificationObjectKey: domainObject }; - [self.cache cacheObject:domainObject]; [self.queue transactionAsync:^(FMDatabase *database) { - [self.table insertDTO:domainObject.basicDTO database:database]; - ZENDeliverNotificationOnMainThread(ZENObjectRepositoryObjectAddedNotification, self, notificationData); + if ([self.table insertDTO:domainObject.basicDTO database:database]) { + [self.cache cacheObject:domainObject]; + ZENDeliverNotificationOnMainThread(ZENObjectRepositoryObjectAddedNotification, self, notificationData); + } ZENCallAsyncContinueBlockOnThreadPool(completion); }]; } @@ -263,10 +264,11 @@ - (void)deleteObject:(ZENDomainObject *)domainObject ZENObjectRepositoryNotificationObjectKey: domainObject }; - [self.cache removeObject:domainObject.identifier]; [self.queue transactionAsync:^(FMDatabase *database) { - [self.table deleteByIdentifier:domainObject.identifier database:database]; - ZENDeliverNotificationOnMainThread(ZENObjectRepositoryObjectDeletedNotification, self, notificationData); + if ([self.table deleteByIdentifier:domainObject.identifier database:database]) { + [self.cache removeObject:domainObject.identifier]; + ZENDeliverNotificationOnMainThread(ZENObjectRepositoryObjectDeletedNotification, self, notificationData); + } ZENCallAsyncContinueBlockOnThreadPool(completion); }]; } From 4978a4ff837c2842acf13fcb72a9f89743c4f21e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:52:59 -0500 Subject: [PATCH 35/46] Check getResourceValue return values in zen_volumeInfoForUUID --- CoreZen/Categories/NSURL+CoreZen.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CoreZen/Categories/NSURL+CoreZen.m b/CoreZen/Categories/NSURL+CoreZen.m index 94e8803..12b079d 100644 --- a/CoreZen/Categories/NSURL+CoreZen.m +++ b/CoreZen/Categories/NSURL+CoreZen.m @@ -47,9 +47,11 @@ + (BOOL)zen_volumeInfoForUUID:(NSUUID *)volumeUUID if ([volumeURL getResourceValue:&uuid forKey:NSURLVolumeUUIDStringKey error:&error] && [uuid isEqual:volumeUUIDString]) { - [volumeURL getResourceValue:outVolumeName forKey:NSURLVolumeLocalizedNameKey error:&error]; - [volumeURL getResourceValue:outVolumeURL forKey:NSURLVolumeURLKey error:&error]; - return YES; + if ([volumeURL getResourceValue:outVolumeName forKey:NSURLVolumeLocalizedNameKey error:&error] && + [volumeURL getResourceValue:outVolumeURL forKey:NSURLVolumeURLKey error:&error]) { + return YES; + } + return NO; } } return NO; From 4078e81297a81d344cbce34b70603740a6dd8f22 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:54:28 -0500 Subject: [PATCH 36/46] Check FMDatabase open return value in database queue --- CoreZen/Database/DatabaseQueue.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CoreZen/Database/DatabaseQueue.m b/CoreZen/Database/DatabaseQueue.m index 3a1194d..a7ba83e 100644 --- a/CoreZen/Database/DatabaseQueue.m +++ b/CoreZen/Database/DatabaseQueue.m @@ -105,7 +105,10 @@ - (FMDatabase *)threadDatabase { database = [FMDatabase databaseWithPath:self.databaseKey]; } - [database open]; + if (![database open]) { + NSLog(@"ERROR: Failed to open database at %@", self.databaseURL); + return nil; + } [database executeUpdate:@"PRAGMA synchronous = 1;"]; [database setShouldCacheStatements:YES]; From 22462137be49f575a8974b63bad30cd53c40d65e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:55:57 -0500 Subject: [PATCH 37/46] Check cancellation token in vacuumAsync --- CoreZen/Database/DatabaseQueue.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CoreZen/Database/DatabaseQueue.m b/CoreZen/Database/DatabaseQueue.m index a7ba83e..f830d8f 100644 --- a/CoreZen/Database/DatabaseQueue.m +++ b/CoreZen/Database/DatabaseQueue.m @@ -167,9 +167,11 @@ - (void)fetchSync:(ZENDatabaseBlock)fetchBlock { - (void)vacuumAsync { [self.workQueue async:^(ZENWorkQueueToken *canceled) { - @autoreleasepool { - FMDatabase *database = self.threadDatabase; - [database executeUpdate:@"VACUUM;"]; + if (!canceled.canceled) { + @autoreleasepool { + FMDatabase *database = self.threadDatabase; + [database executeUpdate:@"VACUUM;"]; + } } }]; } From eadf903b2be17c27f9e62c7bf6d19f2029143737 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:56:29 -0500 Subject: [PATCH 38/46] Declare NSString notification constants as const --- CoreZen/Domain/ObjectRepository.h | 8 ++++---- CoreZen/Domain/ObjectRepository.m | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CoreZen/Domain/ObjectRepository.h b/CoreZen/Domain/ObjectRepository.h index 805cb26..d777325 100644 --- a/CoreZen/Domain/ObjectRepository.h +++ b/CoreZen/Domain/ObjectRepository.h @@ -107,8 +107,8 @@ typedef enum : NSUInteger { @end -extern NSString *ZENObjectRepositoryNotificationObjectKey; +extern NSString * const ZENObjectRepositoryNotificationObjectKey; -extern NSString *ZENObjectRepositoryObjectAddedNotification; -extern NSString *ZENObjectRepositoryObjectUpdatedNotification; -extern NSString *ZENObjectRepositoryObjectDeletedNotification; +extern NSString * const ZENObjectRepositoryObjectAddedNotification; +extern NSString * const ZENObjectRepositoryObjectUpdatedNotification; +extern NSString * const ZENObjectRepositoryObjectDeletedNotification; diff --git a/CoreZen/Domain/ObjectRepository.m b/CoreZen/Domain/ObjectRepository.m index 427b4ac..e115019 100644 --- a/CoreZen/Domain/ObjectRepository.m +++ b/CoreZen/Domain/ObjectRepository.m @@ -298,7 +298,7 @@ - (void)zen_removeObserver:(id)observer { @end -NSString *ZENObjectRepositoryNotificationObjectKey = @"ZENObjectRepositoryNotificationObject"; -NSString *ZENObjectRepositoryObjectAddedNotification = @"ZENObjectRepositoryObjectAddedNotification"; -NSString *ZENObjectRepositoryObjectUpdatedNotification = @"ZENObjectRepositoryObjectUpdatedNotification"; -NSString *ZENObjectRepositoryObjectDeletedNotification = @"ZENObjectRepositoryObjectDeletedNotification"; +NSString * const ZENObjectRepositoryNotificationObjectKey = @"ZENObjectRepositoryNotificationObject"; +NSString * const ZENObjectRepositoryObjectAddedNotification = @"ZENObjectRepositoryObjectAddedNotification"; +NSString * const ZENObjectRepositoryObjectUpdatedNotification = @"ZENObjectRepositoryObjectUpdatedNotification"; +NSString * const ZENObjectRepositoryObjectDeletedNotification = @"ZENObjectRepositoryObjectDeletedNotification"; From ce5d8971beb0028ad8393de79e3079450d61ab3c Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:57:17 -0500 Subject: [PATCH 39/46] Use copy attribute for NSString property in DatabaseQueue --- CoreZen/Database/DatabaseQueue.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Database/DatabaseQueue.m b/CoreZen/Database/DatabaseQueue.m index f830d8f..750b970 100644 --- a/CoreZen/Database/DatabaseQueue.m +++ b/CoreZen/Database/DatabaseQueue.m @@ -20,7 +20,7 @@ - (instancetype)initWithURL:(NSURL *)URL; - (FMDatabase *)threadDatabase; @property (nonatomic, strong, readonly) NSURL *databaseURL; -@property (nonatomic, strong, readonly) NSString *databaseKey; +@property (nonatomic, copy, readonly) NSString *databaseKey; @property (nonatomic, strong, readonly) ZENWorkQueue *workQueue; @end; From 0b60e3be6b53a8d56b59dae5f609b5bb3d75ab33 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:59:35 -0500 Subject: [PATCH 40/46] Rename initCommon to setupCommon to avoid ARC init method family --- CoreZen/Cache/ObjectCache.m | 8 ++++---- CoreZen/Media/Views/MediaPlayerCTIView.m | 4 ++-- CoreZen/Media/Views/MediaPlayerPeekView.m | 8 ++++---- CoreZen/Media/Views/MediaPlayerView.m | 8 ++++---- CoreZen/Queue/WorkQueue.m | 8 ++++---- CoreZen/Views/ArchivedView.h | 4 ++-- CoreZen/Views/ArchivedView.m | 6 +++--- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CoreZen/Cache/ObjectCache.m b/CoreZen/Cache/ObjectCache.m index df5c3a6..007e472 100644 --- a/CoreZen/Cache/ObjectCache.m +++ b/CoreZen/Cache/ObjectCache.m @@ -18,7 +18,7 @@ @interface ZENObjectCache () - (instancetype)initWeakObjectCache; - (instancetype)initStrongObjectCache; -- (void)initCommon:(NSString *)queueLabel; +- (void)setupCommon:(NSString *)queueLabel; @end @@ -28,7 +28,7 @@ - (instancetype)init { return [self initWeakObjectCache]; } -- (void)initCommon:(NSString *)queueLabel { +- (void)setupCommon:(NSString *)queueLabel { static atomic_uint_fast64_t nextIdentifier = 0; uint64_t cacheID = atomic_fetch_add(&nextIdentifier, 1); @@ -41,7 +41,7 @@ - (instancetype)initWeakObjectCache { if (self) { _mapTable = [NSMapTable strongToWeakObjectsMapTable]; - [self initCommon:@"ZENWeakObjectCache"]; + [self setupCommon:@"ZENWeakObjectCache"]; } return self; } @@ -55,7 +55,7 @@ - (instancetype)initStrongObjectCache { if (self) { _mapTable = [NSMapTable strongToStrongObjectsMapTable]; - [self initCommon:@"ZENStrongObjectCache"]; + [self setupCommon:@"ZENStrongObjectCache"]; } return self; } diff --git a/CoreZen/Media/Views/MediaPlayerCTIView.m b/CoreZen/Media/Views/MediaPlayerCTIView.m index 3b123c1..aef3953 100644 --- a/CoreZen/Media/Views/MediaPlayerCTIView.m +++ b/CoreZen/Media/Views/MediaPlayerCTIView.m @@ -40,8 +40,8 @@ - (NSString *)archivedViewName { return @"ZENMediaPlayerCTIView"; } -- (void)initCommon { - [super initCommon]; +- (void)setupCommon { + [super setupCommon]; self.scrubbing = NO; self.previewing = NO; diff --git a/CoreZen/Media/Views/MediaPlayerPeekView.m b/CoreZen/Media/Views/MediaPlayerPeekView.m index 730013e..a675d44 100644 --- a/CoreZen/Media/Views/MediaPlayerPeekView.m +++ b/CoreZen/Media/Views/MediaPlayerPeekView.m @@ -9,13 +9,13 @@ @interface ZENMediaPlayerPeekView () -- (void)initCommon; +- (void)setupCommon; @end @implementation ZENMediaPlayerPeekView -- (void)initCommon { +- (void)setupCommon { self.wantsLayer = YES; self.layer.cornerRadius = 4; self.layer.masksToBounds = YES; @@ -33,7 +33,7 @@ - (void)initCommon { - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } @@ -41,7 +41,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect { - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } diff --git a/CoreZen/Media/Views/MediaPlayerView.m b/CoreZen/Media/Views/MediaPlayerView.m index 288245b..d8fff1f 100644 --- a/CoreZen/Media/Views/MediaPlayerView.m +++ b/CoreZen/Media/Views/MediaPlayerView.m @@ -12,13 +12,13 @@ @interface ZENMediaPlayerView () -- (void)initCommon; +- (void)setupCommon; @end @implementation ZENMediaPlayerView -- (void)initCommon { +- (void)setupCommon { ZENMPVViewLayer *viewLayer = [ZENMPVViewLayer new]; viewLayer.playerView = self; @@ -46,7 +46,7 @@ - (void)initCommon { - (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat *)format { self = [super initWithFrame:frameRect pixelFormat:format]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } @@ -54,7 +54,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect pixelFormat:(NSOpenGLPixelFormat - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } diff --git a/CoreZen/Queue/WorkQueue.m b/CoreZen/Queue/WorkQueue.m index 6f7bef6..17f9ad7 100644 --- a/CoreZen/Queue/WorkQueue.m +++ b/CoreZen/Queue/WorkQueue.m @@ -95,7 +95,7 @@ @interface ZENWorkQueue () @property (nonatomic, strong, readonly) dispatch_queue_t queue; @property (nonatomic, strong, readonly) ZENWorkQueueToken *terminateToken; -- (void)initCommon:(NSString *)label; +- (void)setupCommon:(NSString *)label; - (instancetype)initWithLabel:(NSString *)label; @@ -106,7 +106,7 @@ - (instancetype)initWithLabel:(NSString *)label @implementation ZENWorkQueue -- (void)initCommon:(NSString *)label { +- (void)setupCommon:(NSString *)label { _label = label; _terminateToken = [ZENWorkQueueToken new]; } @@ -114,7 +114,7 @@ - (void)initCommon:(NSString *)label { - (instancetype)initWithLabel:(NSString *)label { self = [super init]; if (self) { - [self initCommon:label]; + [self setupCommon:label]; _queue = dispatch_queue_create(label.UTF8String, DISPATCH_QUEUE_SERIAL); } return self; @@ -124,7 +124,7 @@ - (instancetype)initWithLabel:(NSString *)label qos:(dispatch_qos_class_t)qos { self = [super init]; if (self) { - [self initCommon:label]; + [self setupCommon:label]; dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qos, 0); _queue = dispatch_queue_create(label.UTF8String, attr); } diff --git a/CoreZen/Views/ArchivedView.h b/CoreZen/Views/ArchivedView.h index 6baa2ef..5da4062 100644 --- a/CoreZen/Views/ArchivedView.h +++ b/CoreZen/Views/ArchivedView.h @@ -14,7 +14,7 @@ // Common init path for both -initWithFrame and -initWithCoder - derived // view may override, just be sure to call this super impl first -- (void)initCommon; +- (void)setupCommon; // rootView should be connected to the nib custom view in Interface Builder @property (nonatomic, weak) IBOutlet NSView *rootView; @@ -23,7 +23,7 @@ @interface ZENArchivedView : NSView -- (void)initCommon; +- (void)setupCommon; @property (nonatomic, weak) IBOutlet NSView *rootView; diff --git a/CoreZen/Views/ArchivedView.m b/CoreZen/Views/ArchivedView.m index 77f8312..13c1123 100644 --- a/CoreZen/Views/ArchivedView.m +++ b/CoreZen/Views/ArchivedView.m @@ -16,7 +16,7 @@ - (NSString *)archivedViewName { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:errorReason userInfo:nil]; } -- (void)initCommon { +- (void)setupCommon { NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSNib *nib = [[NSNib alloc] initWithNibNamed:self.archivedViewName bundle:bundle]; if ([nib instantiateWithOwner:self topLevelObjects:nil]) { @@ -30,7 +30,7 @@ - (void)initCommon { - (instancetype)initWithFrame:(NSRect)frameRect { self = [super initWithFrame:frameRect]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } @@ -38,7 +38,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect { - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { - [self initCommon]; + [self setupCommon]; } return self; } From 9ef1c25063a27d2b0ad1639d762b61a697a49e96 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 19:59:54 -0500 Subject: [PATCH 41/46] Warning fix: showsBaselineSeparator is deprecated --- CoreZen/Preferences/PreferencesWindowController.m | 1 - 1 file changed, 1 deletion(-) diff --git a/CoreZen/Preferences/PreferencesWindowController.m b/CoreZen/Preferences/PreferencesWindowController.m index 391b1e7..ae1a2f2 100644 --- a/CoreZen/Preferences/PreferencesWindowController.m +++ b/CoreZen/Preferences/PreferencesWindowController.m @@ -161,7 +161,6 @@ - (void)showPreferencesWindow { NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"ZENPreferencesWindowController"]; toolbar.allowsUserCustomization = NO; toolbar.autosavesConfiguration = YES; - toolbar.showsBaselineSeparator = YES; toolbar.delegate = self; toolbar.selectedItemIdentifier = self.activeViewController.preferenceIdentifier; From 3a009ed8655b0f567908f2041282cfe09b28ee7b Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 20:01:24 -0500 Subject: [PATCH 42/46] Warning fix: cast to CGBitmapInfo --- CoreZen/Media/LibAV/LibAVRenderController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Media/LibAV/LibAVRenderController.m b/CoreZen/Media/LibAV/LibAVRenderController.m index b8d79fa..b485e58 100644 --- a/CoreZen/Media/LibAV/LibAVRenderController.m +++ b/CoreZen/Media/LibAV/LibAVRenderController.m @@ -216,7 +216,7 @@ - (NSImage *)convertRawFrameToImage:(AVFrame *)frame { size_t width = frame->width; size_t height = frame->height; - CGContextRef bitmapContext = CGBitmapContextCreate(frameData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast); + CGContextRef bitmapContext = CGBitmapContextCreate(frameData, width, height, 8, width * 4, colorSpace, (CGBitmapInfo)kCGImageAlphaPremultipliedLast); CGImageRef bitmap = CGBitmapContextCreateImage(bitmapContext); From 22972bf4b9b14314316286ef6992dcea48009d85 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 20:05:44 -0500 Subject: [PATCH 43/46] Return copy of children array to prevent mutation during enumeration --- CoreZen/Node/Node.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Node/Node.m b/CoreZen/Node/Node.m index 748aae7..c607db9 100644 --- a/CoreZen/Node/Node.m +++ b/CoreZen/Node/Node.m @@ -77,7 +77,7 @@ - (NSUInteger)countChildrenAndDescendants { } - (NSArray *)children { - return self.mutableChildren; + return [self.mutableChildren copy]; } - (NSUInteger)childCount { From 2b0667bd7de6f719c283d8f8c7594c801ee7025e Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 20:06:07 -0500 Subject: [PATCH 44/46] Use self instead of defaultManager in NSFileManager category --- CoreZen/Categories/NSFileManager+CoreZen.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreZen/Categories/NSFileManager+CoreZen.m b/CoreZen/Categories/NSFileManager+CoreZen.m index d8da85f..d69dc16 100644 --- a/CoreZen/Categories/NSFileManager+CoreZen.m +++ b/CoreZen/Categories/NSFileManager+CoreZen.m @@ -15,7 +15,7 @@ - (NSURL *)zen_findOrCreateURLForDirectory:(NSSearchPathDirectory)searchPathDire create:(BOOL)create error:(NSError **)outError { // Search for the directory - NSURL *url = [NSFileManager.defaultManager URLForDirectory:searchPathDirectory inDomain:domain appropriateForURL:nil create:create error:outError]; + NSURL *url = [self URLForDirectory:searchPathDirectory inDomain:domain appropriateForURL:nil create:create error:outError]; if (!url) { return nil; } From 1ae0afdae93d0437474a52cd936ec3eeb18666cb Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 20:06:53 -0500 Subject: [PATCH 45/46] Use strong outlet for top-level nib rootView in ArchivedView --- CoreZen/Views/ArchivedView.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CoreZen/Views/ArchivedView.h b/CoreZen/Views/ArchivedView.h index 5da4062..ece4b09 100644 --- a/CoreZen/Views/ArchivedView.h +++ b/CoreZen/Views/ArchivedView.h @@ -17,7 +17,7 @@ - (void)setupCommon; // rootView should be connected to the nib custom view in Interface Builder -@property (nonatomic, weak) IBOutlet NSView *rootView; +@property (nonatomic, strong) IBOutlet NSView *rootView; @end @@ -25,6 +25,6 @@ - (void)setupCommon; -@property (nonatomic, weak) IBOutlet NSView *rootView; +@property (nonatomic, strong) IBOutlet NSView *rootView; @end From ba689131bee4a831e4a81172f22c6850d73039fc Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Wed, 1 Apr 2026 20:10:22 -0500 Subject: [PATCH 46/46] Use explicit request type enum for frame render disambiguation --- CoreZen/Media/FrameRenderer.h | 6 ++++++ CoreZen/Media/FrameRenderer.m | 2 ++ CoreZen/Media/LibAV/LibAVRenderController.m | 5 ++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CoreZen/Media/FrameRenderer.h b/CoreZen/Media/FrameRenderer.h index af6c059..c69b4a8 100644 --- a/CoreZen/Media/FrameRenderer.h +++ b/CoreZen/Media/FrameRenderer.h @@ -14,6 +14,11 @@ typedef void (^ZENRenderFrameResultsBlock)(ZENRenderedFrame *frame); typedef void (^ZENRenderFramesResultsBlock)(NSArray *frames); +typedef NS_ENUM(NSInteger, ZENFrameRequestType) { + ZENFrameRequestTypeSeconds, + ZENFrameRequestTypePercentage +}; + // Rendered frame result @interface ZENRenderedFrame : NSObject @@ -21,6 +26,7 @@ typedef void (^ZENRenderFramesResultsBlock)(NSArray *frames) @property (nonatomic, strong) NSImage *image; // The requested position of the frame +@property (nonatomic) ZENFrameRequestType requestType; @property (nonatomic) int64_t requestedTimestamp; @property (nonatomic) double requestedSeconds; @property (nonatomic) double requestedPercentage; diff --git a/CoreZen/Media/FrameRenderer.m b/CoreZen/Media/FrameRenderer.m index 38b29b7..8de8e1b 100644 --- a/CoreZen/Media/FrameRenderer.m +++ b/CoreZen/Media/FrameRenderer.m @@ -42,6 +42,7 @@ - (ZENWorkQueueToken *)renderFrameAtSeconds:(double)seconds completion:(ZENRenderFrameResultsBlock)completion { ZENRenderedFrame *frame = [ZENRenderedFrame new]; + frame.requestType = ZENFrameRequestTypeSeconds; frame.requestedSeconds = seconds; return [self.frameRenderController renderFrame:frame size:NSMakeSize(width, height) completion:completion]; @@ -53,6 +54,7 @@ - (ZENWorkQueueToken *)renderFrameAtPercentage:(double)percentage completion:(ZENRenderFrameResultsBlock)completion { ZENRenderedFrame *frame = [ZENRenderedFrame new]; + frame.requestType = ZENFrameRequestTypePercentage; frame.requestedPercentage = percentage; return [self.frameRenderController renderFrame:frame size:NSMakeSize(width, height) completion:completion]; diff --git a/CoreZen/Media/LibAV/LibAVRenderController.m b/CoreZen/Media/LibAV/LibAVRenderController.m index b485e58..6fab2c6 100644 --- a/CoreZen/Media/LibAV/LibAVRenderController.m +++ b/CoreZen/Media/LibAV/LibAVRenderController.m @@ -239,11 +239,10 @@ - (ZENWorkQueueToken *)renderFrame:(ZENRenderedFrame *)renderedFrame double durationSeconds = infoController.durationSeconds; - // Calling code fills in either .requestedSeconds or .requestedPercentage; fill in the other - if (renderedFrame.requestedSeconds > renderedFrame.requestedPercentage) { + if (renderedFrame.requestType == ZENFrameRequestTypeSeconds) { // seconds -> percentage renderedFrame.requestedPercentage = renderedFrame.requestedSeconds / durationSeconds; - } else if (renderedFrame.requestedPercentage > renderedFrame.requestedSeconds) { + } else { // percentage -> seconds renderedFrame.requestedSeconds = durationSeconds * renderedFrame.requestedPercentage; }