Skip to content

Commit bccbd93

Browse files
authored
FileLock encounters a runtime error when the path of the locked file is long (#277)
FileLock constructs the name of the lock file based on the path of the file being locked. When this path is very long, the name of the constructed lock file exceeds `NAME_MAX` (or more specifically the directory entry name length limit of the file system on which the directory that contains it resides, but `NAME_MAX` is a decent approximation on modern file systems commonly in use). This change truncates it to `NAME_MAX` UTF-8 bytes (starting from the end), rounding down to avoid splitting Unicode scalars (but making no effort to avoid splitting clusters, since they don't affect the validity of the filename). Another possible refinement would be to also include a hash of the elided part of the filename — in the interest of avoiding complexity this implementation doesn't do that, and it's not clear that it's needed given that 255 (the common value of `NAME_MAX`) usually results in a unique enough name in any practical circumstances. rdar://87471360
1 parent 3cfb7e2 commit bccbd93

File tree

2 files changed

+20
-5
lines changed

2 files changed

+20
-5
lines changed

Sources/TSCBasic/Lock.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -178,12 +178,21 @@ public final class FileLock {
178178
throw FileSystemError(.notDirectory, lockFilesDirectory)
179179
}
180180
// use the parent path to generate unique filename in temp
181-
var lockFileName = (resolveSymlinks(fileToLock.parentDirectory).appending(component: fileToLock.basename)).components.joined(separator: "_")
181+
var lockFileName = (resolveSymlinks(fileToLock.parentDirectory).appending(component: fileToLock.basename)).components.joined(separator: "_") + ".lock"
182182
if lockFileName.hasPrefix(AbsolutePath.root.pathString) {
183183
lockFileName = String(lockFileName.dropFirst(AbsolutePath.root.pathString.count))
184184
}
185-
let lockFilePath = lockFilesDirectory.appending(component: lockFileName + ".lock")
186-
185+
// back off until it occupies at most `NAME_MAX` UTF-8 bytes but without splitting scalars
186+
// (we might split clusters but it's not worth the effort to keep them together as long as we get a valid file name)
187+
var lockFileUTF8 = lockFileName.utf8.suffix(Int(NAME_MAX))
188+
while String(lockFileUTF8) == nil {
189+
// in practice this will only be a few iterations
190+
lockFileUTF8 = lockFileUTF8.dropFirst()
191+
}
192+
// we will never end up with nil since we have ASCII characters at the end
193+
lockFileName = String(lockFileUTF8) ?? lockFileName
194+
let lockFilePath = lockFilesDirectory.appending(component: lockFileName)
195+
187196
let lock = FileLock(at: lockFilePath)
188197
return try lock.withLock(type: type, body)
189198
}

Tests/TSCBasicTests/FileSystemTests.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -767,6 +767,12 @@ class FileSystemTests: XCTestCase {
767767
let lockFile = tempDir.appending(component: "lockfile")
768768

769769
try _testFileSystemFileLock(fileSystem: localFileSystem, fileA: fileA, fileB: fileB, lockFile: lockFile)
770+
771+
// Test some long and edge case paths. We arrange to split between the C and the Cedilla if NAME_MAX is 255.
772+
let longEdgeCase1 = tempDir.appending(component: String(repeating: "Façade! ", count: Int(NAME_MAX)).decomposedStringWithCanonicalMapping)
773+
try localFileSystem.withLock(on: longEdgeCase1, type: .exclusive, {})
774+
let longEdgeCase2 = tempDir.appending(component: String(repeating: "🏁", count: Int(NAME_MAX)).decomposedStringWithCanonicalMapping)
775+
try localFileSystem.withLock(on: longEdgeCase2, type: .exclusive, {})
770776
}
771777
}
772778

0 commit comments

Comments
 (0)