Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

262 lines
8.3 KiB
Swift

import Darwin
import Foundation
struct DiskCleanWhitelistRule: Identifiable, Hashable, Sendable {
enum Source: Hashable, Sendable {
case defaultRule
case custom
}
let rawPattern: String
let expandedPattern: String
let source: Source
var id: String { expandedPattern }
}
enum DiskCleanWhitelistValidationError: Error, Equatable, Sendable {
case empty
case relativePath
case traversal
case controlCharacters
case protectedSystemRoot
}
struct DiskCleanWhitelistStore: Sendable {
static let finderMetadataSentinel = "FINDER_METADATA"
static let defaultRulePatterns: [String] = [
"$HOME/Library/Caches/ms-playwright*",
"$HOME/.cache/huggingface*",
"$HOME/.m2/repository/*",
"$HOME/.gradle/caches/*",
"$HOME/.gradle/daemon/*",
"$HOME/.ollama/models/*",
"$HOME/Library/Caches/com.nssurge.surge-mac/*",
"$HOME/Library/Application Support/com.nssurge.surge-mac/*",
"$HOME/Library/Caches/org.R-project.R/R/renv/*",
"$HOME/Library/Caches/pypoetry/virtualenvs*",
"$HOME/Library/Caches/JetBrains*",
"$HOME/Library/Caches/com.jetbrains.toolbox*",
"$HOME/Library/Caches/tealdeer/tldr-pages",
"$HOME/Library/Application Support/JetBrains*",
"$HOME/Library/Caches/com.apple.finder",
"$HOME/Library/Mobile Documents*",
"$HOME/Library/Caches/com.apple.FontRegistry*",
"$HOME/Library/Caches/com.apple.spotlight*",
"$HOME/Library/Caches/com.apple.Spotlight*",
"$HOME/Library/Caches/CloudKit*",
finderMetadataSentinel
]
let homeDirectory: String
private let includeDefaults: Bool
private let customRules: [String]
init(
homeDirectory: String = NSHomeDirectory(),
includeDefaults: Bool = true,
customRules: [String] = []
) {
self.homeDirectory = Self.normalizeSlashes(homeDirectory)
self.includeDefaults = includeDefaults
self.customRules = customRules
}
func expandedRules() -> [DiskCleanWhitelistRule] {
let defaultRules = includeDefaults ? Self.defaultRulePatterns : []
var seenPatterns = Set<String>()
var rules: [DiskCleanWhitelistRule] = []
for rawPattern in defaultRules {
guard let rule = try? makeRule(rawPattern, source: .defaultRule, validate: true) else {
continue
}
append(rule, to: &rules, seenPatterns: &seenPatterns)
}
for rawPattern in customRules {
guard let rule = try? makeRule(rawPattern, source: .custom, validate: true) else {
continue
}
append(rule, to: &rules, seenPatterns: &seenPatterns)
}
return rules
}
func validateCustomRule(_ rawPattern: String) throws -> DiskCleanWhitelistRule {
try makeRule(rawPattern, source: .custom, validate: true)
}
func matchingRule(for path: String) -> DiskCleanWhitelistRule? {
let normalizedPath = Self.normalizeSlashes(Self.stripTrailingSlash(expandHome(in: path)))
guard !normalizedPath.isEmpty else { return nil }
for rule in expandedRules() {
let pattern = Self.normalizeSlashes(Self.stripTrailingSlash(rule.expandedPattern))
if pattern == Self.finderMetadataSentinel {
if URL(fileURLWithPath: normalizedPath).lastPathComponent == ".DS_Store" {
return rule
}
continue
}
let hasGlob = Self.containsGlob(pattern)
if normalizedPath == pattern || Self.fnmatch(pattern: pattern, path: normalizedPath) {
return rule
}
if pattern.hasPrefix(normalizedPath + "/") {
return rule
}
if !hasGlob && normalizedPath.hasPrefix(pattern + "/") {
return rule
}
}
return nil
}
func isWhitelisted(_ path: String) -> Bool {
matchingRule(for: path) != nil
}
private func makeRule(
_ rawPattern: String,
source: DiskCleanWhitelistRule.Source,
validate: Bool
) throws -> DiskCleanWhitelistRule {
let trimmed = rawPattern.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
throw DiskCleanWhitelistValidationError.empty
}
guard !Self.containsControlCharacter(trimmed) else {
throw DiskCleanWhitelistValidationError.controlCharacters
}
if trimmed == Self.finderMetadataSentinel {
return DiskCleanWhitelistRule(
rawPattern: trimmed,
expandedPattern: trimmed,
source: source
)
}
let expanded = Self.normalizeSlashes(Self.stripTrailingSlash(expandHome(in: trimmed)))
guard expanded.hasPrefix("/") else {
throw DiskCleanWhitelistValidationError.relativePath
}
guard !Self.containsTraversalComponent(expanded) else {
throw DiskCleanWhitelistValidationError.traversal
}
if validate, Self.isProtectedSystemRoot(expanded) {
throw DiskCleanWhitelistValidationError.protectedSystemRoot
}
return DiskCleanWhitelistRule(
rawPattern: trimmed,
expandedPattern: expanded,
source: source
)
}
private func expandHome(in pattern: String) -> String {
if pattern == "~" || pattern == "$HOME" || pattern == "${HOME}" {
return homeDirectory
}
if pattern.hasPrefix("~/") {
return homeDirectory + String(pattern.dropFirst())
}
if pattern.hasPrefix("$HOME/") {
return homeDirectory + String(pattern.dropFirst("$HOME".count))
}
if pattern.hasPrefix("${HOME}/") {
return homeDirectory + String(pattern.dropFirst("${HOME}".count))
}
return pattern
}
private func append(
_ rule: DiskCleanWhitelistRule,
to rules: inout [DiskCleanWhitelistRule],
seenPatterns: inout Set<String>
) {
guard seenPatterns.insert(rule.expandedPattern).inserted else {
return
}
rules.append(rule)
}
private static func normalizeSlashes(_ path: String) -> String {
var normalized = path
while normalized.contains("//") {
normalized = normalized.replacingOccurrences(of: "//", with: "/")
}
return normalized
}
private static func stripTrailingSlash(_ path: String) -> String {
guard path.count > 1 else { return path }
return String(path.dropLast(path.hasSuffix("/") ? 1 : 0))
}
private static func containsControlCharacter(_ pattern: String) -> Bool {
pattern.unicodeScalars.contains { scalar in
scalar.value < 32 || scalar.value == 127
}
}
private static func containsTraversalComponent(_ pattern: String) -> Bool {
pattern.split(separator: "/", omittingEmptySubsequences: false).contains("..")
}
private static func containsGlob(_ pattern: String) -> Bool {
pattern.rangeOfCharacter(from: CharacterSet(charactersIn: "*?[")) != nil
}
private static func fnmatch(pattern: String, path: String) -> Bool {
pattern.withCString { patternPointer in
path.withCString { pathPointer in
Darwin.fnmatch(patternPointer, pathPointer, 0) == 0
}
}
}
private static func isProtectedSystemRoot(_ pattern: String) -> Bool {
let path = stripTrailingGlob(from: stripTrailingSlash(pattern))
if path == "/" {
return true
}
let protectedRoots = [
"/System",
"/bin",
"/sbin",
"/usr",
"/etc",
"/private",
"/var",
"/Library"
]
return protectedRoots.contains { root in
path == root || path.hasPrefix(root + "/")
}
}
private static func stripTrailingGlob(from pattern: String) -> String {
guard let globRange = pattern.rangeOfCharacter(from: CharacterSet(charactersIn: "*?[")) else {
return pattern
}
return stripTrailingSlash(String(pattern[..<globRange.lowerBound]))
}
}