问题描述
想象一下 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
我喜欢记录可靠地处理所有这些复杂问题的方法,目标是高效(即快速)。
解决方法
有两种方法可以检查两个路径(或它们的文件 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];
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
。)
案件有多种不同的处理方式:
- 用户可能手动输入了名称,大小写错误。
- 您之前存储了路径,但用户在此期间重命名了文件的大小写。您的路径仍将标识同一个文件,但现在情况是错误的,相等路径的比较可能会失败,具体取决于您如何获得与之比较的其他路径。
仅当您从文件系统操作中获得路径时,您不能错误地指定路径的任何部分(即错误的大小写),您不需要获取规范路径,而只需调用 {{1} } 然后比较它们的路径是否相等(不需要不区分大小写的选项)。
否则,为了安全起见,从这样的 URL 获取规范路径:
standardizingPath
如果您的路径存储在 String 而不是 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"
}
(Apple Docs)。但这既不能解决不正确的大小写,也不能分解字符,这可能会导致问题,如 aforementioned gist 所示。
因此,从 String 创建文件 URL 然后使用上述方法获取规范路径更安全,或者更好的是,使用 lstat() 解决方案来比较文件 ID,如上所示。
还有一个 BSD 函数可以从 C 字符串中获取规范路径:stringByStandardizingPath
。但是,这并不安全,因为它不会将卷组中不同路径的情况(如问题中所示)解析为相同的字符串。因此,为此应避免使用此功能。