Files
Objective-See Foundation c8a0f6498f bug fixes/project cleanup
v2.2.0
friends updates
removed error reporting
removed old code (pre-macOS 11)

...more changes / improvements soon!
2025-04-30 14:16:30 -07:00

1222 lines
28 KiB
Objective-C

//
// Utilities.m
// TaskExplorer
//
// Created by Patrick Wardle on 2/7/15.
// Copyright (c) 2015 Objective-See. All rights reserved.
//
#import "Consts.h"
#import "Utilities.h"
#import <dlfcn.h>
#import <signal.h>
#import <unistd.h>
#import <syslog.h>
#import <libproc.h>
#import <sys/sysctl.h>
#import <Security/Security.h>
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonDigest.h>
#import <CoreServices/CoreServices.h>
#import <Collaboration/Collaboration.h>
#import <SystemConfiguration/SystemConfiguration.h>
//get OS's major or minor version
SInt32 getVersion(OSType selector)
{
//version
// ->major or minor
SInt32 version = -1;
//get version info
if(noErr != Gestalt(selector, &version))
{
//reset version
version = -1;
//err
goto bail;
}
//bail
bail:
return version;
}
/*
//get the signing info of a file
NSDictionary* extractSigningInfo(NSString* path)
{
//info dictionary
NSMutableDictionary* signingStatus = nil;
//code
SecStaticCodeRef staticCode = NULL;
//status
OSStatus status = !STATUS_SUCCESS;
//signing information
CFDictionaryRef signingInformation = NULL;
//cert chain
NSArray* certificateChain = nil;
//index
NSUInteger index = 0;
//cert
SecCertificateRef certificate = NULL;
//common name on chert
CFStringRef commonName = NULL;
//init signing status
signingStatus = [NSMutableDictionary dictionary];
//create static code
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
//save signature status
signingStatus[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
//sanity check
if(STATUS_SUCCESS != status)
{
//err msg
//syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: SecStaticCodeCreateWithPath() failed on %s with %d", [path UTF8String], status);
//bail
goto bail;
}
//check signature
status = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSDoNotValidateResources, NULL, NULL);
//(re)save signature status
signingStatus[KEY_SIGNATURE_STATUS] = [NSNumber numberWithInt:status];
//if file is signed
// ->grab signing authorities
if(STATUS_SUCCESS == status)
{
//grab signing authorities
status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &signingInformation);
//sanity check
if(STATUS_SUCCESS != status)
{
//err msg
//syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: SecCodeCopySigningInformation() failed on %s with %d", [path UTF8String], status);
//bail
goto bail;
}
//determine if binary is signed by Apple
signingStatus[KEY_SIGNING_IS_APPLE] = [NSNumber numberWithBool:isApple(path)];
}
//error
// ->not signed, or something else, so no need to check cert's names
else
{
//bail
goto bail;
}
//init array for certificate names
signingStatus[KEY_SIGNING_AUTHORITIES] = [NSMutableArray array];
//get cert chain
certificateChain = [(__bridge NSDictionary*)signingInformation objectForKey:(__bridge NSString*)kSecCodeInfoCertificates];
//handle case there is no cert chain
// ->adhoc? (/Library/Frameworks/OpenVPN.framework/Versions/Current/bin/openvpn-service)
if(0 == certificateChain.count)
{
//set
[signingStatus[KEY_SIGNING_AUTHORITIES] addObject:@"signed, but no signing authorities (adhoc?)"];
}
//got cert chain
// ->add each to list
else
{
//get name of all certs
for(index = 0; index < certificateChain.count; index++)
{
//extract cert
certificate = (__bridge SecCertificateRef)([certificateChain objectAtIndex:index]);
//get common name
status = SecCertificateCopyCommonName(certificate, &commonName);
//skip ones that error out
if( (STATUS_SUCCESS != status) ||
(NULL == commonName))
{
//skip
continue;
}
//save
[signingStatus[KEY_SIGNING_AUTHORITIES] addObject:(__bridge NSString*)commonName];
//release name
CFRelease(commonName);
}
}
//bail
bail:
//free signing info
if(NULL != signingInformation)
{
//free
CFRelease(signingInformation);
}
//free static code
if(NULL != staticCode)
{
//free
CFRelease(staticCode);
}
return signingStatus;
}
//determine if a file is signed by Apple proper
BOOL isApple(NSString* path)
{
//flag
BOOL isApple = NO;
//code
SecStaticCodeRef staticCode = NULL;
//signing reqs
SecRequirementRef requirementRef = NULL;
//status
OSStatus status = -1;
//create static code
status = SecStaticCodeCreateWithPath((__bridge CFURLRef)([NSURL fileURLWithPath:path]), kSecCSDefaultFlags, &staticCode);
if(STATUS_SUCCESS != status)
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: SecStaticCodeCreateWithPath() failed on %s with %d", [path UTF8String], status);
//bail
goto bail;
}
//create req string w/ 'anchor apple'
// (3rd party: 'anchor apple generic')
status = SecRequirementCreateWithString(CFSTR("anchor apple"), kSecCSDefaultFlags, &requirementRef);
if( (STATUS_SUCCESS != status) ||
(requirementRef == NULL) )
{
//err msg
syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: SecRequirementCreateWithString() failed on %s with %d", [path UTF8String], status);
//bail
goto bail;
}
//check if file is signed by apple by checking if it conforms to req string
// note: ignore 'errSecCSBadResource' as lots of signed apple files return this issue :/
status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, requirementRef);
if( (STATUS_SUCCESS != status) &&
(errSecCSBadResource != status) )
{
//bail
// ->just means app isn't signed by apple
goto bail;
}
//ok, happy (SecStaticCodeCheckValidity() didn't fail)
// ->file is signed by Apple
isApple = YES;
//bail
bail:
//free req reference
if(NULL != requirementRef)
{
//free
CFRelease(requirementRef);
}
//free static code
if(NULL != staticCode)
{
//free
CFRelease(staticCode);
}
return isApple;
}
*/
//given a directory and a filter predicate
// ->return all matches
NSArray* directoryContents(NSString* directory, NSString* predicate)
{
//(unfiltered) directory contents
NSArray* directoryContents = nil;
//matches
NSArray* matches = nil;
//get (unfiltered) directory contents
directoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directory error:nil];
//filter out matches
if(nil != predicate)
{
//filter
matches = [directoryContents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:predicate]];
}
//no need to filter
else
{
//no filter
matches = directoryContents;
}
return matches;
}
//given a path to binary
// parse it back up to find app's bundle
NSBundle* findAppBundle(NSString* binaryPath)
{
//app's bundle
NSBundle* appBundle = nil;
//app's path
NSString* appPath = nil;
//first just try full path
appPath = binaryPath;
//try to find the app's bundle/info dictionary
do
{
//try to load app's bundle
appBundle = [NSBundle bundleWithPath:appPath];
//check for match
// ->binary path's match
if( (nil != appBundle) &&
(YES == [appBundle.executablePath isEqualToString:binaryPath]))
{
//all done
break;
}
//always unset bundle var since it's being returned
// ->and at this point, its not a match
appBundle = nil;
//remove last part
// ->will try this next
appPath = [appPath stringByDeletingLastPathComponent];
//scan until we get to root
// ->of course, loop will be exited if app info dictionary is found/loaded
} while( (nil != appPath) &&
(YES != [appPath isEqualToString:@"/"]) &&
(YES != [appPath isEqualToString:@""]) );
return appBundle;
}
//hash a file
// ->md5 and sha1
NSDictionary* hashFile(NSString* filePath)
{
//file hashes
NSDictionary* hashes = nil;
//file's contents
NSData* fileContents = nil;
//hash digest (md5)
uint8_t digestMD5[CC_MD5_DIGEST_LENGTH] = {0};
//md5 hash as string
NSMutableString* md5 = nil;
//hash digest (sha1)
uint8_t digestSHA1[CC_SHA1_DIGEST_LENGTH] = {0};
//sha1 hash as string
NSMutableString* sha1 = nil;
//index var
NSUInteger index = 0;
//init md5 hash string
md5 = [NSMutableString string];
//init sha1 hash string
sha1 = [NSMutableString string];
//load file
if(nil == (fileContents = [NSData dataWithContentsOfFile:filePath]))
{
//err msg
//syslog(LOG_ERR, "OBJECTIVE-SEE ERROR: couldn't load %s to hash", [filePath UTF8String]);
//bail
goto bail;
}
//md5 it
CC_MD5(fileContents.bytes, (unsigned int)fileContents.length, digestMD5);
//convert to NSString
// ->iterate over each bytes in computed digest and format
for(index=0; index < CC_MD5_DIGEST_LENGTH; index++)
{
//format/append
[md5 appendFormat:@"%02lX", (unsigned long)digestMD5[index]];
}
//sha1 it
CC_SHA1(fileContents.bytes, (unsigned int)fileContents.length, digestSHA1);
//convert to NSString
// ->iterate over each bytes in computed digest and format
for(index=0; index < CC_SHA1_DIGEST_LENGTH; index++)
{
//format/append
[sha1 appendFormat:@"%02lX", (unsigned long)digestSHA1[index]];
}
//init hash dictionary
hashes = @{KEY_HASH_MD5: md5, KEY_HASH_SHA1: sha1};
//bail
bail:
return hashes;
}
//get app's version
// ->extracted from Info.plist
NSString* getAppVersion()
{
//read and return 'CFBundleVersion' from bundle
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
}
//convert a textview to a clickable hyperlink
void makeTextViewHyperlink(NSTextField* textField, NSURL* url)
{
//hyperlink
NSMutableAttributedString *hyperlinkString = nil;
//range
NSRange range = {0};
//init hyper link
hyperlinkString = [[NSMutableAttributedString alloc] initWithString:textField.stringValue];
//init range
range = NSMakeRange(0, [hyperlinkString length]);
//start editing
[hyperlinkString beginEditing];
//add url
[hyperlinkString addAttribute:NSLinkAttributeName value:url range:range];
//make it blue
[hyperlinkString addAttribute:NSForegroundColorAttributeName value:[NSColor blueColor] range:NSMakeRange(0, [hyperlinkString length])];
//underline
[hyperlinkString addAttribute:
NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSSingleUnderlineStyle] range:NSMakeRange(0, [hyperlinkString length])];
//done editing
[hyperlinkString endEditing];
//set text
[textField setAttributedStringValue:hyperlinkString];
return;
}
//set the color of an attributed string
NSMutableAttributedString* setStringColor(NSAttributedString* string, NSColor* color)
{
//colored string
NSMutableAttributedString *coloredString = nil;
//alloc/init colored string from existing one
coloredString = [[NSMutableAttributedString alloc] initWithAttributedString:string];
//set color
[coloredString addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, [coloredString length])];
return coloredString;
}
//exec a process with args
// if 'shouldWait' is set, wait and return stdout/in and termination status
NSMutableDictionary* execTask(NSString* binaryPath, NSArray* arguments, BOOL shouldWait)
{
//task
NSTask* task = nil;
//output pipe for stdout
NSPipe* stdOutPipe = nil;
//output pipe for stderr
NSPipe* stdErrPipe = nil;
//read handle for stdout
NSFileHandle* stdOutReadHandle = nil;
//read handle for stderr
NSFileHandle* stdErrReadHandle = nil;
//results dictionary
NSMutableDictionary* results = nil;
//output for stdout
NSMutableData *stdOutData = nil;
//output for stderr
NSMutableData *stdErrData = nil;
//init dictionary for results
results = [NSMutableDictionary dictionary];
//init task
task = [NSTask new];
//only setup pipes if wait flag is set
if(YES == shouldWait)
{
//init stdout pipe
stdOutPipe = [NSPipe pipe];
//init stderr pipe
stdErrPipe = [NSPipe pipe];
//init stdout read handle
stdOutReadHandle = [stdOutPipe fileHandleForReading];
//init stderr read handle
stdErrReadHandle = [stdErrPipe fileHandleForReading];
//init stdout output buffer
stdOutData = [NSMutableData data];
//init stderr output buffer
stdErrData = [NSMutableData data];
//set task's stdout
task.standardOutput = stdOutPipe;
//set task's stderr
task.standardError = stdErrPipe;
}
//set task's path
task.launchPath = binaryPath;
//set task's args
if(nil != arguments)
{
//set
task.arguments = arguments;
}
//wrap task launch
@try
{
//launch
[task launch];
}
@catch(NSException *exception)
{
//bail
goto bail;
}
//no need to wait
// can just bail w/ no output
if(YES != shouldWait)
{
//bail
goto bail;
}
//read in stdout/stderr
while(YES == [task isRunning])
{
//accumulate stdout
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
//accumulate stderr
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
}
//grab any leftover stdout
[stdOutData appendData:[stdOutReadHandle readDataToEndOfFile]];
//grab any leftover stderr
[stdErrData appendData:[stdErrReadHandle readDataToEndOfFile]];
//add stdout
if(0 != stdOutData.length)
{
//add
results[STDOUT] = stdOutData;
}
//add stderr
if(0 != stdErrData.length)
{
//add
results[STDERR] = stdErrData;
}
//add exit code
results[EXIT_CODE] = [NSNumber numberWithInteger:task.terminationStatus];
bail:
return results;
}
//wait until a window is non nil
// ->then make it modal
void makeModal(NSWindowController* windowController)
{
//wait up to 1 second window to be non-nil
// ->then make modal
for(int i=0; i<20; i++)
{
//can make it modal once we have a window
if(nil != windowController.window)
{
//make modal on main thread
dispatch_sync(dispatch_get_main_queue(), ^{
//make app front
[NSApp activateIgnoringOtherApps:YES];
//modal
[[NSApplication sharedApplication] runModalForWindow:windowController.window];
});
//all done
break;
}
//nap
[NSThread sleepForTimeInterval:0.05f];
}//until 1 second
return;
}
//given a pid, get its parent (ppid)
pid_t getParentID(int pid)
{
//parent id
pid_t parentID = -1;
//kinfo_proc struct
struct kinfo_proc processStruct = {0};
//size
size_t procBufferSize = sizeof(processStruct);
//syscall result
int sysctlResult = -1;
//init mib
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
//make syscall
sysctlResult = sysctl(mib, sizeof(mib)/sizeof(*mib), &processStruct, &procBufferSize, NULL, 0);
//check if got ppid
if( (STATUS_SUCCESS == sysctlResult) &&
(0 != procBufferSize) )
{
//save ppid
parentID = processStruct.kp_eproc.e_ppid;
}
return parentID;
}
//get path to XPC service
NSString* getPath2XPC()
{
//path to XPC service
NSString* xpcService = nil;
//build path
xpcService = [NSString stringWithFormat:@"%@/Contents/XPCServices/%@", [[NSBundle mainBundle] bundlePath], XPC_SERVICE];
//make sure its there
if(YES != [[NSFileManager defaultManager] fileExistsAtPath:xpcService])
{
//nope
// ->nil out
xpcService = nil;
}
return xpcService;
}
//get path to kernel
NSString* path2Kernel()
{
//kernel path
NSString* kernel;
//check Yosemite's location first
if(YES == [[NSFileManager defaultManager] fileExistsAtPath:KERNEL_YOSEMITE])
{
//set
kernel = KERNEL_YOSEMITE;
}
//go w/ older location
else
{
//set
kernel = KERNEL_PRE_YOSEMITE;
}
return kernel;
}
//determine if process is (still) alive
BOOL isAlive(pid_t targetPID)
{
//flag
BOOL isAlive = YES;
//reset errno
errno = 0;
//'management info base' array
int mib[4] = {0};
//kinfo proc
struct kinfo_proc procInfo = {0};
//try 'kill' with 0
// ->no harm done, but will fail with 'ESRCH' if process is dead
kill(targetPID, 0);
//dead proc -> 'ESRCH'
// ->'No such process'
if(ESRCH == errno)
{
//dead
isAlive = NO;
//bail
goto bail;
}
//size
size_t size = 0;
//init mib
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = targetPID;
//init size
size = sizeof(procInfo);
//get task's flags
// ->allows to check for zombies
if(0 == sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &size, NULL, 0))
{
//check for zombies
if(((procInfo.kp_proc.p_stat) & SZOMB) == SZOMB)
{
//dead
isAlive = NO;
//bail
goto bail;
}
}
//bail
bail:
return isAlive;
}
//check if computer has network connection
BOOL isNetworkConnected()
{
//flag
BOOL isConnected = NO;
//sock addr stuct
struct sockaddr zeroAddress = {0};
//reachability ref
SCNetworkReachabilityRef reachabilityRef = NULL;
//reachability flags
SCNetworkReachabilityFlags flags = 0;
//reachable flag
BOOL isReachable = NO;
//connection required flag
BOOL connectionRequired = NO;
//ensure its cleared out
bzero(&zeroAddress, sizeof(zeroAddress));
//set size
zeroAddress.sa_len = sizeof(zeroAddress);
//set family
zeroAddress.sa_family = AF_INET;
//create reachability ref
reachabilityRef = SCNetworkReachabilityCreateWithAddress(NULL, (const struct sockaddr*)&zeroAddress);
//sanity check
if(NULL == reachabilityRef)
{
//bail
goto bail;
}
//get flags
if(TRUE != SCNetworkReachabilityGetFlags(reachabilityRef, &flags))
{
//bail
goto bail;
}
//set reachable flag
isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
//set connection required flag
connectionRequired = ((flags & kSCNetworkFlagsConnectionRequired) != 0);
//finally
// ->determine if network is available
isConnected = (isReachable && !connectionRequired) ? YES : NO;
//bail
bail:
//cleanup
if(NULL != reachabilityRef)
{
//release
CFRelease(reachabilityRef);
}
return isConnected;
}
//set or unset button's highlight
void buttonAppearance(NSTableView* table, NSEvent* event, BOOL shouldReset)
{
//mouse point
NSPoint mousePoint = {0};
//row index
NSUInteger rowIndex = -1;
//current row
NSTableCellView* currentRow = nil;
//tag
NSUInteger tag = 0;
//button
NSButton* button = nil;
//image name
NSString* imageName = nil;
//extract tag
tag = [((NSDictionary*)event.userData)[@"tag"] unsignedIntegerValue];
//restore button back to default (visual) state
if(YES == shouldReset)
{
//set image name
// ->'info'
if(TABLE_ROW_INFO_BUTTON == tag)
{
//set
imageName = @"info";
}
//set image name
// ->'info'
else if(TABLE_ROW_SHOW_BUTTON == tag)
{
//set
imageName = @"show";
}
}
//highlight button
else
{
//set image name
// ->'info'
if(TABLE_ROW_INFO_BUTTON == tag)
{
//set
imageName = @"infoOver";
}
//set image name
// ->'info'
else if(TABLE_ROW_SHOW_BUTTON == tag)
{
//set
imageName = @"showOver";
}
}
//grab mouse point
mousePoint = [table convertPoint:[event locationInWindow] fromView:nil];
//compute row indow
rowIndex = [table rowAtPoint:mousePoint];
//sanity check
if(-1 == rowIndex)
{
//bail
goto bail;
}
//get row that's about to be selected
currentRow = [table viewAtColumn:0 row:rowIndex makeIfNecessary:YES];
//get button
// ->tag id of button, passed in userData var
button = [currentRow viewWithTag:[((NSDictionary*)event.userData)[@"tag"] unsignedIntegerValue]];
if(nil == button)
{
//bail
goto bail;
}
//restore default button image
// ->for 'info' and 'show' buttons
if(nil != imageName)
{
//set image
[button setImage:[NSImage imageNamed:imageName]];
}
//bail
bail:
return;
}
//check if remote process is i386
BOOL Is32Bit(pid_t targetPID)
{
//info struct
struct proc_bsdshortinfo procInfo = {0};
//flag
BOOL isI386 = NO;
//get proc info
if(proc_pidinfo(targetPID, PROC_PIDT_SHORTBSDINFO, 0, &procInfo, PROC_PIDT_SHORTBSDINFO_SIZE) <= 0)
{
//error
goto bail;
}
//check 64bit process flag
if(PROC_FLAG_LP64 != (procInfo.pbsi_flags & PROC_FLAG_LP64))
{
//not x86_64
// ->thus, i386
isI386 = YES;
}
//bail
bail:
return isI386;
}
//check if app is translocated
// ->based on http://lapcatsoftware.com/articles/detect-app-translocation.html
NSURL* getUnTranslocatedURL()
{
//orignal URL
NSURL* untranslocatedURL = nil;
//function def for 'SecTranslocateIsTranslocatedURL'
Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error);
//function def for 'SecTranslocateCreateOriginalPathForURL'
CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error);
//flag for API request
bool isTranslocated = false;
//handle for security framework
void *handle = NULL;
//app path
NSURL* appPath = nil;
//init app's path
appPath = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
//check ignore pre-macOS Sierra
if(floor(NSAppKitVersionNumber) <= APPKIT_VERSION_10_11)
{
//bail
goto bail;
}
//open security framework
handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
if(NULL == handle)
{
//bail
goto bail;
}
//get 'SecTranslocateIsTranslocatedURL' API
mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL");
if(NULL == mySecTranslocateIsTranslocatedURL)
{
//bail
goto bail;
}
//get
mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL");
if(NULL == mySecTranslocateCreateOriginalPathForURL)
{
//bail
goto bail;
}
//invoke it
if(true != mySecTranslocateIsTranslocatedURL((__bridge CFURLRef)appPath, &isTranslocated, NULL))
{
//bail
goto bail;
}
//bail if app isn't translocated
if(true != isTranslocated)
{
//bail
goto bail;
}
//get original URL
untranslocatedURL = (__bridge NSURL*)mySecTranslocateCreateOriginalPathForURL((__bridge CFURLRef)appPath, NULL);
//bail
bail:
//close handle
if(NULL != handle)
{
//close
dlclose(handle);
}
return untranslocatedURL;
}
//get all user
// includes name/home directory
NSMutableDictionary* allUsers()
{
//users
NSMutableDictionary* users = nil;
//query
CSIdentityQueryRef query = nil;
//query results
CFArrayRef results = NULL;
//error
CFErrorRef error = NULL;
//identiry
CBIdentity* identity = NULL;
//alloc dictionary
users = [NSMutableDictionary dictionary];
//init query
query = CSIdentityQueryCreate(NULL, kCSIdentityClassUser, CSGetLocalIdentityAuthority());
//exec query
if(true != CSIdentityQueryExecute(query, 0, &error))
{
//bail
goto bail;
}
//grab results
results = CSIdentityQueryCopyResults(query);
//process all results
// add user and home directory
for (int i = 0; i < CFArrayGetCount(results); ++i)
{
//grab identity
identity = [CBIdentity identityWithCSIdentity:(CSIdentityRef)CFArrayGetValueAtIndex(results, i)];
//add user
users[identity.UUIDString] = @{USER_NAME:identity.posixName, USER_DIRECTORY:NSHomeDirectoryForUser(identity.posixName)};
}
bail:
//release results
if(NULL != results)
{
//release
CFRelease(results);
}
//release query
if(NULL != query)
{
//release
CFRelease(query);
}
return users;
}
//give a list of paths
// convert any `~` to all or current user
NSMutableArray* expandPaths(const __strong NSString* const paths[], int count)
{
//expanded paths
NSMutableArray* expandedPaths = nil;
//(current) path
const NSString* path = nil;
//all users
NSMutableDictionary* users = nil;
//grab all users
users = allUsers();
//alloc list
expandedPaths = [NSMutableArray array];
//iterate/expand
for(NSInteger i = 0; i < count; i++)
{
//grab path
path = paths[i];
//no `~`?
// just add and continue
if(YES != [path hasPrefix:@"~"])
{
//add as is
[expandedPaths addObject:path];
//next
continue;
}
//handle '~' case
// root? add each user
if(0 == geteuid())
{
//add each user
for(NSString* user in users)
{
[expandedPaths addObject:[users[user][USER_DIRECTORY] stringByAppendingPathComponent:[path substringFromIndex:1]]];
}
}
//otherwise
// just convert to current user
else
{
[expandedPaths addObject:[path stringByExpandingTildeInPath]];
}
}
return expandedPaths;
}
//dark mode?
BOOL isDarkMode(void)
{
return [NSApp.effectiveAppearance.name isEqualToString:NSAppearanceNameDarkAqua];
}
//bring an app to foreground (to get an icon in the dock) or background
void transformProcess(ProcessApplicationTransformState location)
{
//process serial no
ProcessSerialNumber processSerialNo;
//init process stuct
// ->high to 0
processSerialNo.highLongOfPSN = 0;
//init process stuct
// ->low to self
processSerialNo.lowLongOfPSN = kCurrentProcess;
//transform to foreground
TransformProcessType(&processSerialNo, location);
return;
}
//check if file is in shared cache
// uses private _dyld_shared_cache_contains_path API
BOOL isInSharedCache(NSString* path)
{
if (@available(macOS 11.0, *)) {
return _dyld_shared_cache_contains_path(path.UTF8String);
}
return NO;
}