【问题标题】:How to I determine whether two file paths (or file URLs) identify the same file or directory on macOS?如何确定两个文件路径(或文件 URL)是否在 macOS 上标识相同的文件或目录?
【发布时间】:2021-07-02 05:22:40
【问题描述】:

想象一下 macOS 上两个路径的这个简单示例:

  • /etc/hosts
  • /private/etc/hosts

两者都指向同一个文件。但是你如何确定呢?

另一个例子:

  • ~/Desktop
  • /Users/yourname/Desktop

或者在不区分大小写的文件系统上混合大小写怎么办:

  • /Volumes/external/my file
  • /Volumes/External/My File

甚至这个:

  • /Applications/Über.app

这里:“Ü”可以指定为两种unicode composition 格式(NFD、NFC)。有关使用 (NS)URL API 时可能发生这种情况的示例,请参阅this gist of mine

从 macOS 10.15 (Catalina) 开始,另外还有 firmlinks 将卷组中的一个卷链接到另一个卷。同一个 FS 对象的路径可以写成:

  • /Applications/Find Any File.app
  • /System/Volumes/Data/Applications/Find Any File.app

我喜欢记录可靠地处理所有这些复杂问题的方法,目标是高效(即快速)。

【问题讨论】:

    标签: macos filesystems nsurl nsfilemanager


    【解决方案1】:

    有两种方法可以检查两个路径(或其文件 URL)是否指向同一个文件系统项:

    • 比较他们的路径。这需要先准备好路径。
    • 比较它们的 ID(inode)。这总体上更安全,因为它避免了 unicode 复杂性和错误大小写的所有并发症。

    比较文件 ID

    在 ObjC 中,这相当容易(注意:据此,知识渊博的 Apple 开发人员不应依赖 [NSURL fileReferenceURL],因此此代码使用更简洁的方式):

    NSString *p1 = @"/etc/hosts";
    NSString *p2 = @"/private/etc/hosts";
    NSURL *url1 = [NSURL fileURLWithPath:p1];
    NSURL *url2 = [NSURL fileURLWithPath:p2];
    
    id ref1 = nil, ref2 = nil;
    [url1 getResourceValue:&ref1 forKey:NSURLFileResourceIdentifierKey error:nil];
    [url2 getResourceValue:&ref2 forKey:NSURLFileResourceIdentifierKey error:nil];
    
    BOOL equal = [ref1 isEqual:ref2];
    

    Swift 中的等价物(注意:不要使用fileReferenceURL,参见this bug report):

    let p1 = "/etc/hosts"
    let p2 = "/private/etc/hosts"
    let url1 = URL(fileURLWithPath: p1)
    let url2 = URL(fileURLWithPath: p2)
    
    let ref1 = try url1.resourceValues(forKeys[.fileResourceIdentifierKey])
                       .fileResourceIdentifier
    let ref2 = try url2.resourceValues(forKeys[.fileResourceIdentifierKey])
                       .fileResourceIdentifier
    let equal = ref1?.isEqual(ref2) ?? false
    

    两种解决方案都在底层使用 BSD 函数 lstat,因此您也可以用纯 C 编写:

    static bool paths_are_equal (const char *p1, const char *p2) {
      struct stat stat1, stat2;
      int res1 = lstat (p1, &stat1);
      int res2 = lstat (p2, &stat2);
      return (res1 == 0 && res2 == 0) &&
             (stat1.st_dev == stat2.st_dev) && (stat1.st_ino == stat2.st_ino);
    }
    

    但是,请注意 warning 关于使用这些文件引用的信息:

    此标识符的值不会在系统重新启动后保持不变。

    这主要用于卷 ID,但也可能影响不支持持久文件 ID 的文件系统上的文件 ID。

    比较路径

    要比较路径,您必须首先获取它们的规范路径

    如果不这样做,就无法确定case是否正确,进而导致比较代码非常复杂。 (详见using NSURLCanonicalPathKey。)

    案件有不同的处理方式:

    • 用户可能手动输入了名称,但大小写错误。
    • 您之前存储了路径,但同时用户已重命名文件的大小写。您的路径仍会识别同一个文件,但现在情况是错误的,相同路径的比较可能会失败,具体取决于您获得与之比较的其他路径的方式。

    仅当您从文件系统操作中获取路径时,您无法错误地指定路径的任何部分(即使用错误的大小写),您不需要获取规范路径,只需调用 standardizingPath 和然后比较它们的路径是否相等(不需要不区分大小写的选项)。

    否则,为了安全起见,请从这样的 URL 获取规范路径:

    import Foundation
    
    let uncleanPath = "/applications"
    let url = URL(fileURLWithPath: uncleanPath)
    
    if let resourceValues = try? url.resourceValues(forKeys: [.canonicalPathKey]),
        let resolvedPath = resourceValues.canonicalPath {
            print(resolvedPath) // gives "/Applications"
        }
    

    如果您的路径存储在字符串而不是 URL 对象中,您可以调用 stringByStandardizingPath (Apple Docs)。但这既不会解决不正确的大小写,也不会分解字符,这可能会导致aforementioned gist中所示的问题。

    因此,更安全的是从字符串创建文件 URL,然后使用上述方法获取规范路径,或者更好的是,使用 lstat() 解决方案来比较文件 ID,如上所示。

    还有一个 BSD 函数可以从 C 字符串中获取规范路径:realpath()。但是,这并不安全,因为它不会将卷组中不同路径的情况(如问题所示)解析为相同的字符串。因此,应避免使用此功能。

    【讨论】:

      猜你喜欢
      • 2018-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-17
      • 1970-01-01
      • 2021-12-02
      • 1970-01-01
      相关资源
      最近更新 更多