如何确定两个文件路径(或文件 URL)是否标识 macOS 上的同一个文件或目录?

Tho*_*ann 5 filesystems macos nsurl nsfilemanager

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

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

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

另一个例子:

  • ~/Desktop
  • /Users/yourname/Desktop

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

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

甚至这个:

  • /Applications/Über.app

此处:“Ü”可以用两种unicode 组合格式(NFD、NFC)指定。有关使用 (NS)URL API 时可能发生这种情况的示例,请参阅我的这个要点

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

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

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

Tho*_*ann 9

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

  • 比较他们的路径。这需要先准备好路径。
  • 比较它们的 ID(索引节点)。这总体上更安全,因为它避免了 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];
Run Code Online (Sandbox Code Playgroud)

Swift 中的等效项(注意:不要使用fileReferenceURL,请参阅此错误报告):

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
Run Code Online (Sandbox Code Playgroud)

两种解决方案都在后台使用了 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);
}
Run Code Online (Sandbox Code Playgroud)

但是,请注意有关使用此类文件引用的警告

此标识符的值在系统重新启动时不是持久的。

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

比较路径

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

如果不这样做,就无法确定大小写是否正确,进而会导致非常复杂的比较代码。(有关详细信息,请参阅使用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"
    }
Run Code Online (Sandbox Code Playgroud)

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

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

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