Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Quicksilver/Code-QuickStepCore/QSObject_FileHandling.m
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,20 @@ - (NSString *)kindOfObject:(QSObject *)object {
return nil;
}

/* The location the Finder presents a file at: /Applications for system apps
* like FindMy.app, the path itself for everything else */
static NSString *QSDisplayPathForPath(NSString *path) {
return [[[[NSURL fileURLWithPath:path] URLByMappingSystemApplicationsToLocalDomain] path] stringByAbbreviatingWithTildeInPath];
}

- (NSString *)detailsOfObject:(QSObject *)object {
NSArray *theFiles = [object arrayForType:QSFilePathType];
if ([theFiles count] == 1) {
NSString *path = [theFiles lastObject];
if ([object isAlias] && ![object isUbiquitousItem]) { // isAlias returns YES for symlink or alias
// Symlink file
if (QSTypeConformsTo([object fileUTI], (NSString *)kUTTypeSymLink)) {
return [[path stringByResolvingSymlinksInPath] stringByAbbreviatingWithTildeInPath];
return QSDisplayPathForPath([path stringByResolvingSymlinksInPath]);
}
// Finder alias file
NSURL *fileURL = [NSURL fileURLWithPath:path];
Expand Down Expand Up @@ -169,7 +175,7 @@ - (NSString *)detailsOfObject:(QSObject *)object {
}

// normal file
return [path stringByAbbreviatingWithTildeInPath];
return QSDisplayPathForPath(path);

} else if ([theFiles count] > 1) {
return [[theFiles arrayByPerformingSelector:@selector(lastPathComponent)] componentsJoinedByString:@", "];
Expand Down
36 changes: 36 additions & 0 deletions Quicksilver/Code-QuickStepFoundation/NSURL_BLTRExtensions.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,42 @@

@end

@interface NSURL (QSCanonicalPath)

/**
* The location the Finder shows this file at
*
* @return a new NSURL under the local /Applications folder, or self
* @discussion Finder presents the system domain's Applications
* directories (/System/Applications and the App cryptex that backs
* /Applications symlinks like Safari) merged into the local
* /Applications folder, so e.g. /System/Applications/FindMy.app is
* shown in /Applications. Paths outside those directories, and paths
* inside a bundle within them, are returned unchanged. The mapped path
* may not exist on disk (FindMy.app has no /Applications entry), so
* use it for display, not file access.
*/
- (NSURL *)URLByMappingSystemApplicationsToLocalDomain;

/**
* The user-visible location of a file that may be reachable through
* multiple paths
*
* @return a new NSURL for the same file at its user-visible path, or self
* @discussion System applications shipped in a cryptex (e.g. Safari)
* are reported by Launch Services and directory scans at their backing
* location (/System/Volumes/Preboot/Cryptexes/App/... or
* /System/Cryptexes/App/...) rather than the path users see in the
* Finder (/Applications/Safari.app). There is no direct API for this
* mapping (see https://developer.apple.com/forums/thread/745673); it
* is derived from the system's Applications directories, and the
* result is only used when it exists and refers to the same file
* (NSURLFileResourceIdentifierKey), so this always returns a real path.
*/
- (NSURL *)URLByResolvingToUserVisiblePath;

@end

@interface NSURL (QSBookmarkHelpers)
+ (instancetype)URLByResolvingBookmarkAtURL:(NSURL *)bookmarkURL options:(NSURLBookmarkResolutionOptions)options bookmarkDataIsStale:(BOOL *)isStale error:(NSError **)error;
- (BOOL)writeBookmarkToURL:(NSURL *)destinationURL options:(NSURLBookmarkFileCreationOptions)options error:(NSError **)error;
Expand Down
75 changes: 75 additions & 0 deletions Quicksilver/Code-QuickStepFoundation/NSURL_BLTRExtensions.m
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,81 @@ - (NSURL *)URLByReallyResolvingSymlinksInPath {

@end

/* The system domain's Applications directories in every path form: as reported
* by NSSearchPathForDirectoriesInDomains, plus their symlink-resolved forms
* (e.g. /System/Cryptexes/App is a symlink to /System/Volumes/Preboot/Cryptexes/App,
* the form directory enumerators report) */
static NSArray *QSSystemApplicationsDirectories(void) {
static NSArray *directories = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSMutableSet *set = [NSMutableSet set];
for (NSString *dir in NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSSystemDomainMask, YES)) {
[set addObject:dir];
[set addObject:[dir stringByResolvingSymlinksInPath]];
}
directories = [set allObjects];
});
return directories;
}

/* YES if any directory between root and the last path component is a bundle */
static BOOL QSPathHasBundleAncestor(NSString *root, NSArray *components, NSUInteger fromIndex) {
NSString *ancestor = root;
for (NSUInteger i = fromIndex; i + 1 < [components count]; i++) {
ancestor = [ancestor stringByAppendingPathComponent:components[i]];
NSNumber *isPackage = nil;
[[NSURL fileURLWithPath:ancestor] getResourceValue:&isPackage forKey:NSURLIsPackageKey error:NULL];
if ([isPackage boolValue]) return YES;
}
return NO;
}

@implementation NSURL (QSCanonicalPath)

- (NSURL *)URLByMappingSystemApplicationsToLocalDomain {
if (![self isFileURL]) return self;

NSString *path = [self path];
NSArray *components = nil;
for (NSString *systemApps in QSSystemApplicationsDirectories()) {
if (![path hasPrefix:systemApps]) continue;
if (!components) components = [path pathComponents];

// Must be a descendant on a component boundary: /System/ApplicationsFoo
// shares the prefix string but isn't inside /System/Applications
NSArray *systemAppsComponents = [systemApps pathComponents];
NSUInteger prefixCount = [systemAppsComponents count];
if ([components count] <= prefixCount) continue;
if (![[components subarrayWithRange:NSMakeRange(0, prefixCount)] isEqualToArray:systemAppsComponents]) continue;

// Finder only merges the folder hierarchy; paths inside a bundle are not remapped
if (QSPathHasBundleAncestor(systemApps, components, prefixCount)) return self;

NSString *localApps = [NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES) firstObject];
NSArray *relativeComponents = [components subarrayWithRange:NSMakeRange(prefixCount, [components count] - prefixCount)];
return [NSURL fileURLWithPathComponents:[[localApps pathComponents] arrayByAddingObjectsFromArray:relativeComponents]];
}
return self;
}

- (NSURL *)URLByResolvingToUserVisiblePath {
NSURL *mappedURL = [self URLByMappingSystemApplicationsToLocalDomain];
if (mappedURL == self) return self; // not under a system Applications directory

// Only adopt the mapped path if it exists and refers to the same file
// (following symlinks, since the user-visible path may be a symlink to the
// backing location, like /Applications/Safari.app)
id selfIdentifier = nil, mappedIdentifier = nil;
[[self URLByResolvingSymlinksInPath] getResourceValue:&selfIdentifier forKey:NSURLFileResourceIdentifierKey error:NULL];
[[mappedURL URLByResolvingSymlinksInPath] getResourceValue:&mappedIdentifier forKey:NSURLFileResourceIdentifierKey error:NULL];
if (!selfIdentifier || !mappedIdentifier || ![selfIdentifier isEqual:mappedIdentifier]) return self;

return mappedURL;
}

@end

@implementation NSURL (QSBookmarkHelpers)
+ (instancetype)URLByResolvingBookmarkAtURL:(NSURL *)bookmarkURL options:(NSURLBookmarkResolutionOptions)options bookmarkDataIsStale:(BOOL *)isStale error:(NSError **)error {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#import "NSWorkspace_BLTRExtensions.h"
#import "NSApplication_BLTRExtensions.h"
#import "NSArray_BLTRExtensions.h"
#import "NSURL_BLTRExtensions.h"
#include <signal.h>
#include <unistd.h>

Expand Down Expand Up @@ -86,7 +88,10 @@ - (BOOL)setComment:(NSString*)comment forFile:(NSString *)path {
- (NSArray *)allApplicationsURLs {
CFArrayRef appURLs = NULL;
_LSCopyAllApplicationURLs(&appURLs);
return (__bridge_transfer NSArray *)appURLs;
NSArray *urls = (__bridge_transfer NSArray *)appURLs;
// Launch Services reports cryptex-shipped apps (e.g. Safari) at their
// backing location; resolve to the user-visible path (#3125)
return [urls arrayByPerformingSelector:@selector(URLByResolvingToUserVisiblePath)];
}

- (NSArray *)allApplications {
Expand Down
11 changes: 10 additions & 1 deletion Quicksilver/PlugIns-Main/QSCorePlugIn/Code/QSDirectoryParser.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "QSDirectoryParser.h"

#import "NDAlias+AliasFile.h"
#import "NSURL_BLTRExtensions.h"


@implementation QSDirectoryParser
Expand Down Expand Up @@ -138,7 +139,15 @@ - (NSArray *)objectsFromPath:(NSString *)path depth:(NSInteger)depth types:(NSAr
}

if (include) {
QSObject *obj = [QSObject fileObjectWithFileURL:theURL];
NSURL *objectURL = theURL;
if (QSTypeConformsTo(type, (NSString *)kUTTypeApplication)) {
// Applications can be scanned via a cryptex backing path
// (e.g. /System/Cryptexes/App/System/Applications/Safari.app);
// catalog them at their user-visible path so every source
// yields the same object (#3125)
objectURL = [objectURL URLByResolvingToUserVisiblePath];
}
Comment thread
skurfer marked this conversation as resolved.
QSObject *obj = [QSObject fileObjectWithFileURL:objectURL];
if (aliasSource) [obj setObject:[aliasSource data] forType:QSAliasDataType];
if (aliasFile) [obj setObject:aliasFile forType:QSAliasFilePathType];
if (obj) [array addObject:obj];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<key>name</key>
<string>Applications</string>
</dict>
<!-- The App cryptex's Applications directory. Apps here (currently
just Safari) also have an /Applications symlink and are picked up
by the Applications preset above, and both scans produce the same
object, so this is a fallback for a future cryptex app shipped
without a symlink. See issues #2932 and #3125. -->
Comment thread
danielcompton marked this conversation as resolved.
<dict>
<key>source</key>
<string>QSFileSystemObjectSource</string>
Expand All @@ -62,7 +67,7 @@
<string>com.apple.application</string>
</array>
<key>folderDepth</key>
<integer>1</integer>
<integer>3</integer>
<key>scanContents</key>
<integer>1</integer>
<key>kind</key>
Expand Down
4 changes: 4 additions & 0 deletions Quicksilver/Quicksilver.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
6615228414F43355006FDCB4 /* PathWildcardsData in Resources */ = {isa = PBXBuildFile; fileRef = 6615228314F43355006FDCB4 /* PathWildcardsData */; };
66448DF214F427B8000FA2E2 /* QSFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1E5F9AF07B1FCFC0044D6EF /* QSFoundation.framework */; };
66448E1314F42A4E000FA2E2 /* TestPathWildcards.m in Sources */ = {isa = PBXBuildFile; fileRef = 66448E1214F42A4E000FA2E2 /* TestPathWildcards.m */; };
664D3125202608170000A001 /* TestNSURLCanonicalPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 664D3125202608170000A000 /* TestNSURLCanonicalPath.m */; };
666E45FF150226200034E60A /* QSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E103F1600647200700447FE0 /* QSCore.framework */; };
7F05F1E20852441C00A8EC0C /* NSObject+BLTRExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F05F1E00852441C00A8EC0C /* NSObject+BLTRExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; };
7F05F1E30852441C00A8EC0C /* NSObject+BLTRExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F05F1E10852441C00A8EC0C /* NSObject+BLTRExtensions.m */; };
Expand Down Expand Up @@ -1259,6 +1260,7 @@
66448D9614F42790000FA2E2 /* QSFoundationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QSFoundationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
66448D9714F42790000FA2E2 /* QSFoundationTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "QSFoundationTests-Info.plist"; sourceTree = "<group>"; };
66448E1214F42A4E000FA2E2 /* TestPathWildcards.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TestPathWildcards.m; path = "Tests/Tests-QSFoundation/TestPathWildcards.m"; sourceTree = "<group>"; };
664D3125202608170000A000 /* TestNSURLCanonicalPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TestNSURLCanonicalPath.m; path = "Tests/Tests-QSFoundation/TestNSURLCanonicalPath.m"; sourceTree = "<group>"; };
666E45EE150225EA0034E60A /* QSCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QSCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
666E45EF150225EA0034E60A /* QSCoreTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "QSCoreTests-Info.plist"; sourceTree = "<group>"; };
66D11C6515022A75002EE6E5 /* QSEffectsTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "QSEffectsTests-Info.plist"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2805,6 +2807,7 @@
children = (
CD661C5319DBD2F3000F3695 /* TestNSApplicationMethods.m */,
66448E1214F42A4E000FA2E2 /* TestPathWildcards.m */,
664D3125202608170000A000 /* TestNSURLCanonicalPath.m */,
4DF329D81EA2C5F0003CD3CE /* TestQSSense.m */,
CD72C74F2877C0D10040B065 /* TestNDHotkey.m */,
);
Expand Down Expand Up @@ -4961,6 +4964,7 @@
4DF329D91EA2C5F0003CD3CE /* TestQSSense.m in Sources */,
CD661C5419DBD2F3000F3695 /* TestNSApplicationMethods.m in Sources */,
66448E1314F42A4E000FA2E2 /* TestPathWildcards.m in Sources */,
664D3125202608170000A001 /* TestNSURLCanonicalPath.m in Sources */,
CD72C7502877C0D10040B065 /* TestNDHotkey.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
35 changes: 35 additions & 0 deletions Quicksilver/Tests/Tests-QSCore/QSFileObjectTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ - (void)testFileUTI
XCTAssertTrue(UTTypeConformsTo((__bridge CFStringRef)[object fileUTI], (__bridge CFStringRef)@"public.script"), @"The fast logout script should be seen as a script by Quicksilver");
}

- (void)testCryptexApplicationDetails
{
// Cryptex-backed apps should show their /Applications location, not a
// backing path under /System/Volumes/Preboot. Issue #3125
NSString *backingPath = [@"/Applications/Safari.app" stringByResolvingSymlinksInPath];
if ([backingPath isEqualToString:@"/Applications/Safari.app"]) {
XCTSkip(@"Safari is not cryptex-backed on this system");
}
QSFileSystemObjectHandler *handler = [[QSFileSystemObjectHandler alloc] init];
QSObject *object = [QSObject fileObjectWithPath:backingPath];
XCTAssertEqualObjects([handler detailsOfObject:object], @"/Applications/Safari.app");

// The /Applications symlink to the cryptex should show itself, not its target
object = [QSObject fileObjectWithPath:@"/Applications/Safari.app"];
XCTAssertEqualObjects([handler detailsOfObject:object], @"/Applications/Safari.app");
}

- (void)testSystemApplicationDetails
{
// Apps in /System/Applications are shown by the Finder in /Applications,
// so that's what details should show. Issue #3125
NSString *terminalPath = @"/System/Applications/Utilities/Terminal.app";
if (![[NSFileManager defaultManager] fileExistsAtPath:terminalPath]) {
XCTSkip(@"Terminal.app is not in /System/Applications on this system");
}
QSFileSystemObjectHandler *handler = [[QSFileSystemObjectHandler alloc] init];
QSObject *object = [QSObject fileObjectWithPath:terminalPath];
XCTAssertEqualObjects([handler detailsOfObject:object], @"/Applications/Utilities/Terminal.app");

// ...but paths inside their bundles are not remapped
NSString *interiorPath = [terminalPath stringByAppendingPathComponent:@"Contents/Info.plist"];
object = [QSObject fileObjectWithPath:interiorPath];
XCTAssertEqualObjects([handler detailsOfObject:object], interiorPath);
}

- (void)testFileObject
{
NSString *path;
Expand Down
66 changes: 66 additions & 0 deletions Quicksilver/Tests/Tests-QSFoundation/TestNSURLCanonicalPath.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// TestNSURLCanonicalPath.m
// Quicksilver
//
// Tests for the NSURL (QSCanonicalPath) category (issue #3125)
//

#import <XCTest/XCTest.h>
#import "NSURL_BLTRExtensions.h"

@interface TestNSURLCanonicalPath : XCTestCase
@end

@implementation TestNSURLCanonicalPath

- (void)testRegularPathsAreUnchanged {
for (NSString *path in @[@"/Applications", NSHomeDirectory(), @"/usr/bin/yes"]) {
NSURL *url = [NSURL fileURLWithPath:path];
XCTAssertEqualObjects([url URLByResolvingToUserVisiblePath].path, path);
XCTAssertEqualObjects([url URLByMappingSystemApplicationsToLocalDomain].path, path);
}
}

- (void)testNonexistentPathIsUnchanged {
NSURL *url = [NSURL fileURLWithPath:@"/Applications/QSDoesNotExist.app"];
XCTAssertEqualObjects([url URLByResolvingToUserVisiblePath], url);
}

- (void)testNonFileURLIsUnchanged {
NSURL *url = [NSURL URLWithString:@"https://qsapp.com/"];
XCTAssertEqualObjects([url URLByResolvingToUserVisiblePath], url);
XCTAssertEqualObjects([url URLByMappingSystemApplicationsToLocalDomain], url);
}

- (void)testCryptexBackedAppResolvesToUserVisiblePath {
// Safari ships in a cryptex; Launch Services and directory scans can
// report it at the backing location instead of the /Applications symlink
NSString *userVisiblePath = @"/Applications/Safari.app";
NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:userVisiblePath]
|| ![manager fileExistsAtPath:@"/System/Cryptexes/App/System/Applications/Safari.app"]) {
XCTSkip(@"Safari is not cryptex-backed on this system");
}
for (NSString *backingPath in @[@"/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app",
@"/System/Cryptexes/App/System/Applications/Safari.app"]) {
NSURL *resolved = [[NSURL fileURLWithPath:backingPath] URLByResolvingToUserVisiblePath];
XCTAssertEqualObjects(resolved.path, userVisiblePath);
}
}

- (void)testSystemApplicationWithoutLocalCounterpart {
NSString *terminalPath = @"/System/Applications/Utilities/Terminal.app";
if (![[NSFileManager defaultManager] fileExistsAtPath:terminalPath]) {
XCTSkip(@"Terminal.app is not in /System/Applications on this system");
}
// The catalog mapping requires an existing file, so the real path is kept...
XCTAssertEqualObjects([[NSURL fileURLWithPath:terminalPath] URLByResolvingToUserVisiblePath].path, terminalPath);
// ...while the Finder-location mapping gives the /Applications location
XCTAssertEqualObjects([[NSURL fileURLWithPath:terminalPath] URLByMappingSystemApplicationsToLocalDomain].path,
@"/Applications/Utilities/Terminal.app");
// Paths inside a bundle are never remapped
NSString *interiorPath = [terminalPath stringByAppendingPathComponent:@"Contents/Info.plist"];
XCTAssertEqualObjects([[NSURL fileURLWithPath:interiorPath] URLByMappingSystemApplicationsToLocalDomain].path, interiorPath);
}

@end