3

I am on OSX, Objective-C.

I have a path/NSURL like

/Users/xxx/Desktop/image2.png

But i pass it to a third party application that excpects finder pathes like

Harddisk:Users:Desktop:image2.png

Is there any method (i can't find) to convert pathes like that or get them out of an NSURL (if possible without string modifying)?

In AppleScript it is

return POSIX file "/Users/xxx/Desktop/image2.png" -->  Harddisk:Users:xxx:Desktop:image2.png

EDIT: This is pretty much the same: Cocoa path string conversion Unfortunately, the method is deprecated...

Pat_Morita
  • 3,355
  • 3
  • 25
  • 36

3 Answers3

2

There is no (easy) alternative at the moment.

The function CFURLCopyFileSystemPath is not deprecated, only the enum case kCFURLHFSPathStyle is deprecated but the raw value 1 is still working and avoids the warning.

I'm using this category of NSString

@implementation NSString (POSIX_HFS)

- (NSString *)hfsPathFromPOSIXPath
{
    CFStringRef hfsPath = CFURLCopyFileSystemPath((CFURLRef)[NSURL fileURLWithPath:self], 1);
    return (NSString *)CFBridgingRelease(hfsPath);
}
@end

The function works also in Swift. The Swift version is a bit more sophisticated and adds the trailing semicolon representing a dictionary implicitly, here as an extension of URL:

extension URL {

  func hfsPath() -> String?
  {
    if let cfpathHFS = CFURLCopyFileSystemPath(self as CFURL, CFURLPathStyle(rawValue: 1)!) { // CFURLPathStyle.CFURLHFSPathStyle)
      let pathHFS = cfpathHFS as String
      do {
        let info = try self.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey])
        let isDirectory = info.isDirectory!
        let isPackage =  info.isPackage!

        if isDirectory && !isPackage {
          return pathHFS + ":" // directory, not package
        }
      } catch _ {}
      return pathHFS
    }
    return nil
  }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • I'm looking for the inverse operation: https://stackoverflow.com/questions/55383444 – Thomas Tempelmann Mar 27 '19 at 17:39
  • I just found out that, at least in 10.13.6, `CFURLCopyFileSystemPath` does not create the correct HFS path if the path contains a "/" in the Finder-visible file name. E.g. the POSIX path "/Folder/FileWithColonAtEnd:" should be turned into "Volname:Folder:FileWithColonAtEnd/" but gets converted into "Volname:Folder:FileWithColonAtEnd", or something where the last char is rather random. – Thomas Tempelmann Sep 15 '19 at 22:19
2

Vadians answer is better than this one - but if vadians method is deprecated, this will be an alternative. Idea is to use applescripts methods to get HFS path called easily with an osascript from an NSString category.

NSString category (credits: https://stackoverflow.com/a/19014463/4591992)

@implementation NSString (ShellExecution)

- (NSString*)runAsCommand {
    NSPipe* pipe = [NSPipe pipe];

    NSTask* task = [[NSTask alloc] init];
    [task setLaunchPath: @"/bin/sh"];
    [task setArguments:@[@"-c", [NSString stringWithFormat:@"%@", self]]];
    [task setStandardOutput:pipe];

    NSFileHandle* file = [pipe fileHandleForReading];
    [task launch];

    NSString* result = [[NSString alloc] initWithData:[file readDataToEndOfFile] encoding:NSUTF8StringEncoding];
    return result;
}

@end

Usage for this case:

NSString* posixToHFS = [NSString stringWithFormat:@"osascript -e 'POSIX file \"%@\" as text'",filePath];
filePath = [posixToHFS runAsCommand];
Pat_Morita
  • 3,355
  • 3
  • 25
  • 36
  • In terms of AppleScript: `info for` of Standard Additions is deprecated since macOS 10.5 Leopard, but it's still working. So what? ;-) – vadian Jul 13 '17 at 20:53
  • 1
    `NSAppleScript` can execute AppleScript. – Willeke Jul 15 '17 at 11:08
  • Spawning a new process every time you want to convert a path, is rather clumsy and inefficient. You could run an AppleScript directly using `NSAppleScript` class, an I think the CoreFoundation conversion methods are not deprecated - or they must have replacements. – Motti Shneor May 17 '22 at 07:05
0

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

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
  • 1
    Looks like NSString's `stringByStandardizingPath` is being used behind the scenes in an inconsistent manner - that will remove trailing slashes. – red_menace Sep 16 '19 at 02:10