In my own testing (on 10.13.6, 10.14.6 and 10.15b7), @vadian's solution doesn't work with paths where a folder name component contains a "/" (when viewed in Finder), which then appears as a ":" in a POSIX path and as a "/" in a HFS path.
Demonstration of the bug
Here's a quick test program that you can build by creating a new "command line" project in Xcode:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *posixPath = @"/EndsInSlash:";
NSURL *url = [NSURL fileURLWithPath:posixPath];
if (url == nil) {
NSLog(@"Oops, this went wrong");
} else {
CFStringRef hfsPath = CFURLCopyFileSystemPath((CFURLRef)url, 1);
NSString *res = (NSString *)CFBridgingRelease (hfsPath);
NSLog(@"HFS path: <%@>", res);
}
}
return 0;
}
When you run it, you'll probably see a correct result printed, i.e. a path that ends in "/". However, that only works if the folder does not exist. So, create a folder named "EndsInSlash/" (not a file!) in your root folder and run the app again - now the resulting path does not end in "/" any more as it should.
Work-around
Below is a "smart" function that uses the faster CFURLCopyFileSystemPath()
function whenever possible, i.e. unless a ":" appears in the POSIX path - in which case it performs the conversion on its own, by splitting up the POSIX path into its components, converting them individually (replacing ":" into "/"), prepending the volume name and then merging the components again. This appears to work fine even on macOS 10.15 (Catalina), despite the deprecation warnings.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
static NSString* stringWithHFSUniStr255(const HFSUniStr255* hfsString)
{
CFStringRef stringRef = FSCreateStringFromHFSUniStr(nil, hfsString);
NSString* result = CFBridgingRelease(stringRef);
return result;
}
NSString* hfsPathFromPOSIXPath (NSString *posixPath)
{
if (posixPath == nil) return @"";
if ([posixPath containsString:@":"]) {
// slow version, but can handle ":" appearing in path components
NSString *result = nil;
FSRef ref;
Boolean isDir;
if (FSPathMakeRef ((const UInt8*)posixPath.UTF8String, &ref, &isDir) == noErr) {
HFSUniStr255 elemName;
FSCatalogInfo catInfo;
NSMutableArray<NSString*> *elems = [NSMutableArray arrayWithCapacity:16];
while (FSGetCatalogInfo (&ref, kFSCatInfoNodeID, &catInfo, &elemName, nil, &ref) == noErr) {
[elems insertObject: stringWithHFSUniStr255(&elemName) atIndex:0];
if (catInfo.nodeID == 2) break;
}
result = [elems componentsJoinedByString:@":"];
}
return result;
} else {
// see https://stackoverflow.com/a/45085776/43615
NSURL *url = [NSURL fileURLWithPath:posixPath];
if (url == nil) {
// could not convert because the path doesn't exist
return nil;
}
CFStringRef hfsPath = CFURLCopyFileSystemPath((CFURLRef)url, kCFURLHFSPathStyle);
return (NSString *)CFBridgingRelease (hfsPath);
}
}
#pragma clang diagnostic pop
See also
Discussion of a related bug with AppleScript, with a work-around: https://forum.latenightsw.com/t/xxx/2097
Bug report filed with Apple: http://www.openradar.me/radar?id=4994410022436864