mirror of
https://github.com/apple/container.git
synced 2026-08-24 02:24:19 -05:00
Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com> Co-authored-by: John Logan <john_logan@apple.com> Co-authored-by: Raj Aryan Singh <rajaryan_singh@apple.com>
157 lines
6.6 KiB
Swift
157 lines
6.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import ContainerizationOS
|
|
import Foundation
|
|
import SystemPackage
|
|
|
|
public class Globber {
|
|
let input: URL
|
|
var results: Set<URL> = .init()
|
|
|
|
public init(_ input: URL) {
|
|
self.input = input
|
|
}
|
|
|
|
public func match(_ pattern: String) throws {
|
|
let adjustedPattern =
|
|
pattern
|
|
.replacingOccurrences(of: #"^\./(?=.)"#, with: "", options: .regularExpression)
|
|
.replacingOccurrences(of: "^\\.[/]?$", with: "*", options: .regularExpression)
|
|
.replacingOccurrences(of: "\\*{2,}[/]", with: "*/**/", options: .regularExpression)
|
|
.replacingOccurrences(of: "[/]\\*{2,}([^/])", with: "/**/*$1", options: .regularExpression)
|
|
.replacingOccurrences(of: "^\\*{2,}([^/])", with: "**/*$1", options: .regularExpression)
|
|
|
|
for child in self.children(of: input) {
|
|
try self.match(input: child, components: adjustedPattern.split(separator: "/").map(String.init))
|
|
}
|
|
}
|
|
|
|
private func match(input: URL, components: [String]) throws {
|
|
if components.isEmpty {
|
|
var dir = input.standardizedFileURL
|
|
|
|
while dir != self.input.standardizedFileURL {
|
|
results.insert(dir)
|
|
guard dir.pathComponents.count > 1 else { break }
|
|
dir.deleteLastPathComponent()
|
|
}
|
|
return self.childrenRecursive(of: input).forEach { results.insert($0) }
|
|
}
|
|
|
|
let head = components.first ?? ""
|
|
let tail = components.tail
|
|
|
|
if head == "**" {
|
|
var tail: [String] = tail
|
|
while tail.first == "**" {
|
|
tail = tail.tail
|
|
}
|
|
try self.match(input: input, components: tail)
|
|
for child in self.children(of: input) {
|
|
try self.match(input: child, components: components)
|
|
}
|
|
return
|
|
}
|
|
|
|
if try glob(input.lastPathComponent, head) {
|
|
try self.match(input: input, components: tail)
|
|
|
|
for child in self.children(of: input) where try glob(child.lastPathComponent, tail.first ?? "") {
|
|
try self.match(input: child, components: tail)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
/// Returns the direct children of `url`, following `url` itself when it is
|
|
/// a directory symlink whose fully-resolved target stays within the match
|
|
/// root. A symlink that escapes the root is treated as having no children
|
|
/// (same as a regular file) so pattern components after it never match —
|
|
/// mirrors the containment check `BuildFSSync` applies before reading.
|
|
///
|
|
/// Children are named by their resolved (physical) path, not by `url`, so
|
|
/// that `walk(root:includePatterns:)`'s later filter — which is driven by
|
|
/// `Archiver.compress`'s own physical directory walk — reliably finds a
|
|
/// matching entry regardless of whether that walk itself follows `url`'s
|
|
/// symlink. `url` is separately inserted into `results` so the symlink
|
|
/// entry is still present in the tar for the builder to resolve the
|
|
/// original path against.
|
|
private func children(of url: URL) -> [URL] {
|
|
// TODO: modifying object state and returning results is odd, rework
|
|
guard let dir = self.resolvedDirectory(of: url) else { return [] }
|
|
if url.isSymlink { self.results.insert(url) }
|
|
return (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))
|
|
?? []
|
|
}
|
|
|
|
/// Recursive form of ``children(of:)``, used once a full pattern (or `**`)
|
|
/// has matched `url` and every descendant needs to be collected. Nested
|
|
/// directory symlinks are resolved and boundary-checked the same way, one
|
|
/// level at a time, via ``FileDescriptorOps/enumerate`` which never follows
|
|
/// symlinks it encounters mid-traversal — only the top-level `url` passed
|
|
/// in here gets the resolve-and-check treatment.
|
|
private func childrenRecursive(of url: URL) -> [URL] {
|
|
guard let dir = self.resolvedDirectory(of: url) else { return [url] }
|
|
if url.isSymlink { self.results.insert(url) }
|
|
guard let fd = try? FileDescriptor.open(FilePath(dir.path), .readOnly, options: .directory) else {
|
|
return [dir]
|
|
}
|
|
defer { try? fd.close() }
|
|
var found: [URL] = [dir]
|
|
try? FileDescriptorOps.enumerate(fd) { relPath, _, _ in
|
|
found.append(dir.appendingPathComponent(relPath.string))
|
|
}
|
|
return found
|
|
}
|
|
|
|
/// Resolves `url` to the real directory whose contents should be listed in
|
|
/// its place. Non-symlinks resolve to themselves. A directory symlink
|
|
/// resolves to its target only if the fully-resolved target is still
|
|
/// within `self.input` (the match root); otherwise `nil`, so callers treat
|
|
/// it as a leaf rather than descending outside the context.
|
|
private func resolvedDirectory(of url: URL) -> URL? {
|
|
guard url.isSymlink else { return url }
|
|
let resolved = url.resolvingSymlinksInPath()
|
|
guard resolved.isDirectory, self.input.parentOf(resolved) else { return nil }
|
|
return resolved
|
|
}
|
|
|
|
func glob(_ input: String, _ pattern: String) throws -> Bool {
|
|
let regexPattern =
|
|
"^"
|
|
+ NSRegularExpression.escapedPattern(for: pattern)
|
|
.replacingOccurrences(of: "\\*", with: "[^/]*")
|
|
.replacingOccurrences(of: "\\?", with: "[^/]")
|
|
.replacingOccurrences(of: "[\\^", with: "[^")
|
|
.replacingOccurrences(of: "\\[", with: "[")
|
|
.replacingOccurrences(of: "\\]", with: "]") + "$"
|
|
|
|
// validate the regex pattern created
|
|
let _ = try Regex(regexPattern)
|
|
return input.range(of: regexPattern, options: .regularExpression) != nil
|
|
}
|
|
}
|
|
|
|
extension [String] {
|
|
var tail: [String] {
|
|
if self.count <= 1 {
|
|
return []
|
|
}
|
|
return Array(self.dropFirst())
|
|
}
|
|
}
|