Cinder27 — iOS 17.0-27.1 vulnerability verification lab: 51 modules, 31 per-CVE POC folders, 11 no-CVE module folders, harness app source
This commit is contained in:
commit
1ea76ab005
89 files changed
+16485
No files matched your search
@@ -0,0 +1,577 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// MARK: - File manager
|
||||
//
|
||||
// Full-featured within what the sandbox actually permits: navigate anywhere readable,
|
||||
// jump by typing a path, bookmark locations, and perform the standard mutations with a
|
||||
// real write probe behind every claim. Nothing here pretends to reach past L1.
|
||||
|
||||
// MARK: Clipboard for copy / move
|
||||
|
||||
final class FileClipboard: ObservableObject {
|
||||
enum Mode { case none, copy, move }
|
||||
@Published private(set) var mode: Mode = .none
|
||||
@Published private(set) var paths: [String] = []
|
||||
|
||||
func stage(_ paths: [String], mode: Mode) {
|
||||
self.paths = paths
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
func clear() {
|
||||
paths = []
|
||||
mode = .none
|
||||
}
|
||||
|
||||
var isEmpty: Bool { paths.isEmpty }
|
||||
}
|
||||
|
||||
// MARK: Bookmarks
|
||||
|
||||
final class Bookmarks: ObservableObject {
|
||||
@Published private(set) var items: [String] = []
|
||||
|
||||
private let key = "cinder27.bookmarks"
|
||||
|
||||
init() {
|
||||
items = UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
if items.isEmpty {
|
||||
items = [
|
||||
Container.documents.path,
|
||||
Container.appSupport.path,
|
||||
Container.work.path,
|
||||
"/System/Library/Frameworks",
|
||||
"/usr/lib",
|
||||
"/private/etc",
|
||||
"/tmp",
|
||||
"/",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
func add(_ path: String) {
|
||||
guard !items.contains(path) else { return }
|
||||
items.append(path)
|
||||
persist()
|
||||
}
|
||||
|
||||
func remove(_ path: String) {
|
||||
items.removeAll { $0 == path }
|
||||
persist()
|
||||
}
|
||||
|
||||
private func persist() {
|
||||
UserDefaults.standard.set(items, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The pane
|
||||
|
||||
struct FilesPane: View {
|
||||
@EnvironmentObject private var browser: FileBrowser
|
||||
@EnvironmentObject private var clipboard: FileClipboard
|
||||
@EnvironmentObject private var bookmarks: Bookmarks
|
||||
|
||||
@State private var jumpShown = false
|
||||
@State private var jumpPath = ""
|
||||
@State private var newFolderShown = false
|
||||
@State private var newFolderName = ""
|
||||
@State private var renameTarget: FileItem?
|
||||
@State private var renameName = ""
|
||||
@State private var preview: FileItem?
|
||||
@State private var info: FileItem?
|
||||
@State private var status: String?
|
||||
@State private var confirmDelete: FileItem?
|
||||
@State private var searchText = ""
|
||||
@State private var selecting = false
|
||||
@State private var selection = Set<String>()
|
||||
@State private var sortMode = 0
|
||||
|
||||
private var visible: [FileItem] {
|
||||
var list = browser.items
|
||||
if !searchText.isEmpty {
|
||||
list = list.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
||||
}
|
||||
switch sortMode {
|
||||
case 1: list.sort { $0.size > $1.size }
|
||||
case 2: list.sort { ($0.modified ?? .distantPast) > ($1.modified ?? .distantPast) }
|
||||
case 3: list.sort { $0.name.localizedStandardCompare($1.name) == .orderedDescending }
|
||||
default: break
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
bookmarkBar
|
||||
pathBar
|
||||
searchBar
|
||||
|
||||
if let status = status {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: status.hasPrefix("✓") ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 10))
|
||||
Text(status).font(T.monoSm).lineLimit(2)
|
||||
Spacer()
|
||||
Button { self.status = nil } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 9, weight: .bold))
|
||||
}
|
||||
}
|
||||
.foregroundColor(status.hasPrefix("✓") ? T.good : T.warn)
|
||||
.padding(.horizontal, 16).padding(.bottom, 6)
|
||||
}
|
||||
|
||||
if let error = browser.error {
|
||||
Text(error).font(T.monoSm).foregroundColor(T.bad)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16).padding(.bottom, 6)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if !clipboard.isEmpty {
|
||||
clipboardBar
|
||||
}
|
||||
|
||||
List {
|
||||
ForEach(visible) { item in
|
||||
row(item)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.refreshable { browser.reload() }
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
Menu {
|
||||
Button { jumpShown = true } label: { Label(t("files.jump"), systemImage: "arrow.right.to.line") }
|
||||
Button { newFolderShown = true } label: { Label(t("files.newFolder"), systemImage: "folder.badge.plus") }
|
||||
Divider()
|
||||
Button { bookmarks.add(browser.path); status = tl("✓ 已加入书签") } label: {
|
||||
Label(t("files.bookmarks"), systemImage: "bookmark")
|
||||
}
|
||||
Divider()
|
||||
Picker("", selection: $sortMode) {
|
||||
Text(t("files.name")).tag(0)
|
||||
Text("Size").tag(1)
|
||||
Text("Date").tag(2)
|
||||
Text("Z→A").tag(3)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}.tint(T.ember)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $jumpShown) { jumpSheet }
|
||||
.sheet(item: $preview) { item in
|
||||
FilePreviewView(item: item).environmentObject(browser)
|
||||
}
|
||||
.sheet(item: $info) { item in
|
||||
FileInfoSheet(item: item, browser: browser)
|
||||
}
|
||||
.alert(t("files.newFolder"), isPresented: $newFolderShown) {
|
||||
TextField(t("files.name"), text: $newFolderName)
|
||||
Button(t("common.ok")) {
|
||||
if let err = browser.makeDirectory(newFolderName) {
|
||||
status = err
|
||||
} else {
|
||||
status = tl("✓ 已创建 ") + "\(newFolderName)"
|
||||
}
|
||||
newFolderName = ""
|
||||
}
|
||||
Button(t("files.cancel"), role: .cancel) { newFolderName = "" }
|
||||
}
|
||||
.alert(t("files.rename"), isPresented: Binding(
|
||||
get: { renameTarget != nil },
|
||||
set: { if !$0 { renameTarget = nil } })) {
|
||||
TextField(t("files.name"), text: $renameName)
|
||||
Button(t("common.ok")) {
|
||||
if let target = renameTarget {
|
||||
if let err = browser.rename(target, to: renameName) {
|
||||
status = err
|
||||
} else {
|
||||
status = "✓ \(t("files.renamed")) → \(renameName)"
|
||||
}
|
||||
}
|
||||
renameTarget = nil
|
||||
}
|
||||
Button(t("files.cancel"), role: .cancel) { renameTarget = nil }
|
||||
}
|
||||
.alert("\(t("files.delete"))?", isPresented: Binding(
|
||||
get: { confirmDelete != nil },
|
||||
set: { if !$0 { confirmDelete = nil } })) {
|
||||
Button(t("files.delete"), role: .destructive) {
|
||||
if let target = confirmDelete {
|
||||
if let err = browser.delete(target) {
|
||||
status = err
|
||||
} else {
|
||||
status = "✓ \(t("files.deleted")) \(target.name)"
|
||||
}
|
||||
}
|
||||
confirmDelete = nil
|
||||
}
|
||||
Button(t("files.cancel"), role: .cancel) { confirmDelete = nil }
|
||||
} message: {
|
||||
Text(confirmDelete?.path ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Bars
|
||||
|
||||
private var bookmarkBar: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 7) {
|
||||
ForEach(bookmarks.items, id: \.self) { path in
|
||||
Button {
|
||||
browser.chdir(path)
|
||||
} label: {
|
||||
Pill(text: (path as NSString).lastPathComponent.isEmpty ? "/" : (path as NSString).lastPathComponent,
|
||||
color: browser.path == path ? T.ember : .white.opacity(0.45))
|
||||
}
|
||||
.contextMenu {
|
||||
Button(t("files.delete"), role: .destructive) { bookmarks.remove(path) }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
private var pathBar: some View {
|
||||
HStack(spacing: 8) {
|
||||
Button { browser.up() } label: {
|
||||
Image(systemName: "arrow.up").font(.system(size: 12, weight: .bold))
|
||||
}.tint(T.ember)
|
||||
|
||||
Button {
|
||||
jumpPath = browser.path
|
||||
jumpShown = true
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Text(browser.path)
|
||||
.font(T.monoSm)
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.head)
|
||||
Image(systemName: "chevron.down").font(.system(size: 8, weight: .bold))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Spacer()
|
||||
|
||||
if browser.writable {
|
||||
Pill(text: t("files.writable"), color: T.good)
|
||||
} else {
|
||||
Pill(text: t("files.readonly"), color: T.warn)
|
||||
}
|
||||
|
||||
Button { browser.reload() } label: {
|
||||
Image(systemName: "arrow.clockwise").font(.system(size: 12, weight: .bold))
|
||||
}.tint(T.ember)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 6)
|
||||
}
|
||||
|
||||
private var searchBar: some View {
|
||||
HStack(spacing: 7) {
|
||||
Image(systemName: "magnifyingglass").font(.system(size: 11)).foregroundColor(.white.opacity(0.3))
|
||||
TextField(t("files.name"), text: $searchText)
|
||||
.font(T.monoSm)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
if !searchText.isEmpty {
|
||||
Button { searchText = "" } label: {
|
||||
Image(systemName: "xmark.circle.fill").font(.system(size: 11))
|
||||
}.tint(.white.opacity(0.3))
|
||||
}
|
||||
Text("\(visible.count)").font(T.monoSm).foregroundColor(.white.opacity(0.25))
|
||||
}
|
||||
.padding(.horizontal, 10).padding(.vertical, 7)
|
||||
.background(RoundedRectangle(cornerRadius: 9).fill(T.panel))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
private var clipboardBar: some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: clipboard.mode == .copy ? "doc.on.doc" : "arrow.right.doc.on.clipboard")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(T.info)
|
||||
Text("\(clipboard.paths.count) · \(clipboard.mode == .copy ? t("files.copy") : t("files.move"))")
|
||||
.font(T.monoSm).foregroundColor(.white.opacity(0.6))
|
||||
Spacer()
|
||||
Button(t("files.paste")) { paste() }
|
||||
.font(.system(size: 11, weight: .semibold)).tint(T.ember)
|
||||
Button(t("files.cancel")) { clipboard.clear() }
|
||||
.font(.system(size: 11)).tint(.white.opacity(0.4))
|
||||
}
|
||||
.padding(.horizontal, 12).padding(.vertical, 8)
|
||||
.background(RoundedRectangle(cornerRadius: 9).fill(T.info.opacity(0.1)))
|
||||
.overlay(RoundedRectangle(cornerRadius: 9).strokeBorder(T.info.opacity(0.3), lineWidth: 1))
|
||||
.padding(.horizontal, 16).padding(.bottom, 8)
|
||||
}
|
||||
|
||||
// MARK: Row
|
||||
|
||||
private func row(_ item: FileItem) -> some View {
|
||||
Button {
|
||||
if item.isDirectory { browser.enter(item) } else { preview = item }
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: item.icon)
|
||||
.font(.system(size: 13))
|
||||
.foregroundColor(item.isDirectory ? T.ember : .white.opacity(0.4))
|
||||
.frame(width: 20)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(item.name)
|
||||
.font(T.body)
|
||||
.foregroundColor(.white.opacity(0.86))
|
||||
.lineLimit(1)
|
||||
HStack(spacing: 6) {
|
||||
if !item.isDirectory { Text(item.displaySize) }
|
||||
if item.isSymlink { Text("→") }
|
||||
if let m = item.modified {
|
||||
Text(FileBrowser.dateFormatter.string(from: m))
|
||||
}
|
||||
}
|
||||
.font(T.monoSm)
|
||||
.foregroundColor(.white.opacity(0.26))
|
||||
}
|
||||
Spacer()
|
||||
if item.isDirectory {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundColor(.white.opacity(0.18))
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowBackground(T.panel)
|
||||
.contextMenu {
|
||||
Button { info = item } label: { Label(t("files.info"), systemImage: "info.circle") }
|
||||
Button {
|
||||
renameTarget = item
|
||||
renameName = item.name
|
||||
} label: { Label(t("files.rename"), systemImage: "pencil") }
|
||||
Button {
|
||||
clipboard.stage([item.path], mode: .copy)
|
||||
status = "✓ \(t("files.copied")) \(item.name)"
|
||||
} label: { Label(t("files.copy"), systemImage: "doc.on.doc") }
|
||||
Button {
|
||||
clipboard.stage([item.path], mode: .move)
|
||||
status = "→ \(t("files.move")) \(item.name)"
|
||||
} label: { Label(t("files.move"), systemImage: "arrow.right.doc.on.clipboard") }
|
||||
Button { duplicate(item) } label: { Label(t("files.duplicate"), systemImage: "plus.square.on.square") }
|
||||
if item.isDirectory {
|
||||
Button {
|
||||
bookmarks.add(item.path)
|
||||
status = tl("✓ 书签 ") + "\(item.name)"
|
||||
} label: { Label(t("files.bookmarks"), systemImage: "bookmark") }
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive) { confirmDelete = item } label: {
|
||||
Label(t("files.delete"), systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) { confirmDelete = item } label: {
|
||||
Label(t("files.delete"), systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
renameTarget = item
|
||||
renameName = item.name
|
||||
} label: { Label(t("files.rename"), systemImage: "pencil") }
|
||||
.tint(T.info)
|
||||
}
|
||||
.swipeActions(edge: .leading) {
|
||||
Button {
|
||||
clipboard.stage([item.path], mode: .copy)
|
||||
} label: { Label(t("files.copy"), systemImage: "doc.on.doc") }
|
||||
.tint(T.ember)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Jump sheet
|
||||
|
||||
private var jumpSheet: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Card {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SectionLabel(text: t("files.path"))
|
||||
TextField("/path/to/directory", text: $jumpPath)
|
||||
.font(T.mono)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.padding(9)
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(T.panelHi))
|
||||
HStack(spacing: 8) {
|
||||
Button(t("files.done")) {
|
||||
browser.chdir(jumpPath)
|
||||
jumpShown = false
|
||||
}
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.tint(T.ember)
|
||||
Spacer()
|
||||
Button("~/Documents") {
|
||||
jumpPath = Container.documents.path
|
||||
}.font(T.monoSm).tint(.white.opacity(0.4))
|
||||
Button("/") { jumpPath = "/" }
|
||||
.font(T.monoSm).tint(.white.opacity(0.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
SectionLabel(text: t("files.bookmarks"))
|
||||
ForEach(bookmarks.items, id: \.self) { path in
|
||||
Button {
|
||||
browser.chdir(path)
|
||||
jumpShown = false
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "folder.fill")
|
||||
.font(.system(size: 11)).foregroundColor(T.ember)
|
||||
Text(Container.shortPath(URL(fileURLWithPath: path)))
|
||||
.font(T.monoSm).foregroundColor(.white.opacity(0.7))
|
||||
.lineLimit(1).truncationMode(.middle)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.background(T.bg.ignoresSafeArea())
|
||||
.navigationTitle(t("files.jump"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(t("files.cancel")) { jumpShown = false }.tint(T.ember)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Operations
|
||||
|
||||
private func paste() {
|
||||
let fm = FileManager.default
|
||||
var ok = 0
|
||||
var failed: [String] = []
|
||||
for source in clipboard.paths {
|
||||
let name = (source as NSString).lastPathComponent
|
||||
let dest = (browser.path as NSString).appendingPathComponent(name)
|
||||
guard dest != source else { continue }
|
||||
do {
|
||||
if fm.fileExists(atPath: dest) {
|
||||
// never silently overwrite
|
||||
let stem = (name as NSString).deletingPathExtension
|
||||
let ext = (name as NSString).pathExtension
|
||||
var n = 2
|
||||
var candidate = dest
|
||||
while fm.fileExists(atPath: candidate) {
|
||||
let newName = ext.isEmpty ? "\(stem) \(n)" : "\(stem) \(n).\(ext)"
|
||||
candidate = (browser.path as NSString).appendingPathComponent(newName)
|
||||
n += 1
|
||||
}
|
||||
if clipboard.mode == .copy {
|
||||
try fm.copyItem(atPath: source, toPath: candidate)
|
||||
} else {
|
||||
try fm.moveItem(atPath: source, toPath: candidate)
|
||||
}
|
||||
} else {
|
||||
if clipboard.mode == .copy {
|
||||
try fm.copyItem(atPath: source, toPath: dest)
|
||||
} else {
|
||||
try fm.moveItem(atPath: source, toPath: dest)
|
||||
}
|
||||
}
|
||||
ok += 1
|
||||
} catch {
|
||||
failed.append("\(name): \(Container.describe(errno))")
|
||||
}
|
||||
}
|
||||
browser.reload()
|
||||
if failed.isEmpty {
|
||||
status = "✓ \(clipboard.mode == .copy ? t("files.copied") : t("files.moved")) \(ok)"
|
||||
clipboard.clear()
|
||||
} else {
|
||||
status = failed.joined(separator: " · ")
|
||||
}
|
||||
}
|
||||
|
||||
private func duplicate(_ item: FileItem) {
|
||||
let fm = FileManager.default
|
||||
let dir = (item.path as NSString).deletingLastPathComponent
|
||||
let stem = (item.name as NSString).deletingPathExtension
|
||||
let ext = (item.name as NSString).pathExtension
|
||||
var n = 2
|
||||
var candidate = ""
|
||||
repeat {
|
||||
let name = ext.isEmpty ? "\(stem) \(n)" : "\(stem) \(n).\(ext)"
|
||||
candidate = (dir as NSString).appendingPathComponent(name)
|
||||
n += 1
|
||||
} while fm.fileExists(atPath: candidate)
|
||||
|
||||
do {
|
||||
try fm.copyItem(atPath: item.path, toPath: candidate)
|
||||
browser.reload()
|
||||
status = "✓ \(t("files.copied")) \((candidate as NSString).lastPathComponent)"
|
||||
} catch {
|
||||
status = Container.describe(errno)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Info sheet
|
||||
|
||||
struct FileInfoSheet: View {
|
||||
let item: FileItem
|
||||
@ObservedObject var browser: FileBrowser
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Card {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
ForEach(Array(browser.fileInfo(item).enumerated()), id: \.offset) { _, row in
|
||||
KeyValueRow(key: row.0, value: row.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !item.isDirectory {
|
||||
Card {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
SectionLabel(text: t("files.hex"))
|
||||
Text(FileLoader.hexDump(at: item.path, limit: 2048) ?? "—")
|
||||
.font(.system(size: 9.5, design: .monospaced))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.background(T.bg.ignoresSafeArea())
|
||||
.navigationTitle(item.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button(t("module.close")) { dismiss() }.tint(T.ember)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user