[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n.swiftpm/\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n"
  },
  {
    "path": "Makefile",
    "content": "install:\n\t\tswift build -c release\n\t\tinstall .build/release/privacy-manifest /usr/local/bin/privacy-manifest\n"
  },
  {
    "path": "Package.resolved",
    "content": "{\n  \"pins\" : [\n    {\n      \"identity\" : \"aexml\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/tadija/AEXML.git\",\n      \"state\" : {\n        \"revision\" : \"38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3\",\n        \"version\" : \"4.6.1\"\n      }\n    },\n    {\n      \"identity\" : \"bluesignals\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/IBM-Swift/BlueSignals.git\",\n      \"state\" : {\n        \"revision\" : \"1f6c49e186c8a4eeef87ba14f2f97b8646559d13\",\n        \"version\" : \"1.0.200\"\n      }\n    },\n    {\n      \"identity\" : \"nanoseconds\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/dominicegginton/Nanoseconds\",\n      \"state\" : {\n        \"revision\" : \"34318d7a13b5b5013102fbe18b36b80368b4dcbd\",\n        \"version\" : \"1.1.2\"\n      }\n    },\n    {\n      \"identity\" : \"pathkit\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kylef/PathKit.git\",\n      \"state\" : {\n        \"revision\" : \"3bfd2737b700b9a36565a8c94f4ad2b050a5e574\",\n        \"version\" : \"1.0.1\"\n      }\n    },\n    {\n      \"identity\" : \"rainbow\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/onevcat/Rainbow\",\n      \"state\" : {\n        \"revision\" : \"e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3\",\n        \"version\" : \"4.0.1\"\n      }\n    },\n    {\n      \"identity\" : \"spectre\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/kylef/Spectre.git\",\n      \"state\" : {\n        \"revision\" : \"26cc5e9ae0947092c7139ef7ba612e34646086c7\",\n        \"version\" : \"0.10.1\"\n      }\n    },\n    {\n      \"identity\" : \"spinner\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/dominicegginton/Spinner\",\n      \"state\" : {\n        \"revision\" : \"16ac0c404320005936579f1d6454bb88dbd8d71a\",\n        \"version\" : \"2.1.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-argument-parser\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-argument-parser\",\n      \"state\" : {\n        \"revision\" : \"8f4d2753f0e4778c76d5f05ad16c74f707390531\",\n        \"version\" : \"1.2.3\"\n      }\n    },\n    {\n      \"identity\" : \"swift-asn1\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-asn1.git\",\n      \"state\" : {\n        \"revision\" : \"c7e239b5c1492ffc3ebd7fbcc7a92548ce4e78f0\",\n        \"version\" : \"1.1.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-certificates\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-certificates.git\",\n      \"state\" : {\n        \"revision\" : \"01d7664523af5c169f26038f1e5d444ce47ae5ff\",\n        \"version\" : \"1.0.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-collections\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-collections.git\",\n      \"state\" : {\n        \"revision\" : \"d029d9d39c87bed85b1c50adee7c41795261a192\",\n        \"version\" : \"1.0.6\"\n      }\n    },\n    {\n      \"identity\" : \"swift-crypto\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-crypto.git\",\n      \"state\" : {\n        \"revision\" : \"629f0b679d0fd0a6ae823d7f750b9ab032c00b80\",\n        \"version\" : \"3.0.0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-driver\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-driver.git\",\n      \"state\" : {\n        \"branch\" : \"release/5.10\",\n        \"revision\" : \"46bd60c4934aa8512061b8182f59dcc5f0a25fd0\"\n      }\n    },\n    {\n      \"identity\" : \"swift-llbuild\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-llbuild.git\",\n      \"state\" : {\n        \"branch\" : \"release/5.10\",\n        \"revision\" : \"fd7c2e0d9279edd023ced6b0a590f8407f5472f9\"\n      }\n    },\n    {\n      \"identity\" : \"swift-package-manager\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-package-manager\",\n      \"state\" : {\n        \"branch\" : \"release/5.10\",\n        \"revision\" : \"a0e7a8aef2989e315d0fe2180a5cbe2b9c8dc150\"\n      }\n    },\n    {\n      \"identity\" : \"swift-system\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-system.git\",\n      \"state\" : {\n        \"revision\" : \"836bc4557b74fe6d2660218d56e3ce96aff76574\",\n        \"version\" : \"1.1.1\"\n      }\n    },\n    {\n      \"identity\" : \"swift-tools-support-core\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/apple/swift-tools-support-core.git\",\n      \"state\" : {\n        \"branch\" : \"release/5.10\",\n        \"revision\" : \"3695ee46daf7604bec9e16337a60aa62cd261774\"\n      }\n    },\n    {\n      \"identity\" : \"xcodeproj\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/tuist/XcodeProj.git\",\n      \"state\" : {\n        \"revision\" : \"313aaf1ad612135b7b0ccf731c86b5c07bf149b5\",\n        \"version\" : \"8.20.0\"\n      }\n    },\n    {\n      \"identity\" : \"yams\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://github.com/jpsim/Yams.git\",\n      \"state\" : {\n        \"revision\" : \"0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3\",\n        \"version\" : \"5.0.6\"\n      }\n    }\n  ],\n  \"version\" : 2\n}\n"
  },
  {
    "path": "Package.swift",
    "content": "// swift-tools-version:5.9\n// The swift-tools-version declares the minimum version of Swift required to build this package.\n\nimport PackageDescription\n\nlet package = Package(\n    name: \"PrivacyManifest\",\n    platforms: [\n        .macOS(.v13)\n    ],\n    products: [\n        .executable(name: \"privacy-manifest\",\n                    targets: [\"PrivacyManifest\"])\n    ],\n    dependencies: [\n        .package(url: \"https://github.com/apple/swift-argument-parser\",\n                 exact: \"1.2.3\"),\n        .package(url: \"https://github.com/apple/swift-package-manager\",\n                 branch: \"release/5.10\"),\n        .package(url: \"https://github.com/tuist/XcodeProj\",\n                 exact: \"8.20.0\"),\n        .package(url: \"https://github.com/dominicegginton/Spinner\",\n                 exact: \"2.1.0\")\n    ],\n    targets: [\n        .executableTarget(\n            name: \"PrivacyManifest\",\n            dependencies: [\n                \"XcodeProj\",\n                \"Spinner\",\n                .product(name: \"ArgumentParser\",\n                         package: \"swift-argument-parser\"),\n                .product(name: \"SwiftPM-auto\", \n                         package: \"swift-package-manager\"),\n            ]\n        )\n    ]\n)\n"
  },
  {
    "path": "README.md",
    "content": "# Privacy Manifest\n\n<p align=\"left\">\n<img src=\"https://img.shields.io/badge/macOS_v13%2B-_?style=flat&logo=apple&label=platform\">\n</p>\n\nPrivacy Manifest CLI tool parses an Xcode project/workspace or a Swift Package\nand attempts to locate calls to Apple's required reason APIs [^1] and detect\nprivacy collected data frameworks [^2].\n\nThe tool detects and parses the source files of the project as well as the\nframeworks added in the Xcode project's Build Phase or in the Swift Package\ndependencies. It also detects any frameworks / static libraries and checks if\nthey are included in the third-party SDK list that Apple has provided [^3].\n\nThe tool does not perform any sort of analysis beyond the simple\nline-by-line check for the existence of the method calls or symbols that\nApple has already published.\n\nThe tool uses a concurrent queue to speed up the parsing process.\n\n## Installation\n\nYou can either use the tool by typing: `swift run privacy-manifest` in the root\ndirectory of the project, or you can install the executable to `/usr/local/bin`\ndirectory so that you can call it from any folder.\n\nCheck out the project and run the following command in the project root \nto install the binary to `/usr/local/bin`.\n\n```sh\nsudo make install\n```\n\n\n## Usage\n\nAfter installing the tool to the `/usr/local/bin` directory, you can invoke it\nfrom any directory using the following command:\n\n```\nprivacy-manifest analyze --project path/to/project --reveal-occurrences --output path\n```\n\nThe `path/to/project` can be a relative or an absolute path to the `.xcodeproj`\nor `Package.swift` file of your project.\n\nThe `--reveal-occurrences` is an optional flag that displays extended information\nregarding the occurrences of the required reason APIs / privacy collected data\nframeworks in your codebase, highlighting the file and the line where a call has\nbeen detected.\n\nThe `--output` flag is optional and if specified, a `PrivacyInfo.xcprivacy`\nproperty list file will be generated to that directory based on the detected\nrequired reason APIs and from the responses of the user.\n\n## Example\n\nBelow is the console output from the [VLC iOS OSS](https://github.com/videolan/vlc-ios).\n\n![Privacy Manifest analyze running for VLC iOS project](https://raw.githubusercontent.com/stelabouras/privacy-manifest/main/.github/privacymanifest-vlc.gif)\n\n## Future implementations\n\nThe tool can output the occurrences report to HTML for better readability.\n\nOn top of that, the list of third-party crash frameworks can be updated so that\nit can inform the user when such framework is detected (there is a related TODO\nin the code).\n\n## Disclaimer\n\nDo not use this tool alone to create your privacy manifest file for your app or\nSDK. You must always double-check the occurrences that the tool displays as the\ntool does not know whether a certain occurrence is included in a comment or on\nan unused piece of code. Furthermore, there might also be cases where something\nhas not been included in the parsing process.\n\nThis tool gives you a high-level overview of the different required reason APIs\nand privacy collected data frameworks your project, workspace or package uses,\nso always do your own research after using this tool, to confirm the findings.\n\n## License\n\nLicensed under Apache License 2.0, see [LICENSE](LICENSE) file.\n\n[^1]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api\n[^2]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests).\n[^3]: https://developer.apple.com/support/third-party-SDK-requirements/\n"
  },
  {
    "path": "Sources/PrivacyManifest/Constants.swift",
    "content": "//\n//  Constants.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nstruct CliSyntaxColor {\n    static let WHITE_BOLD = \"\\u{001B}[0;1m\"\n    static let RED = \"\\u{001B}[0;0;31m\"\n    static let GREEN = \"\\u{001B}[0;32m\"\n    static let YELLOW = \"\\u{001B}[0;33m\"\n    static let BLUE = \"\\u{001B}[0;34m\"\n    static let MAGENTA = \"\\u{001B}[0;35m\"\n    static let CYAN = \"\\u{001B}[0;36m\"\n    static let PINK = \"\\u{001B}[0;91m\"\n    static let GREEN_BRIGHT = \"\\u{001B}[0;92m\"\n    static let YELLOW_BRIGHT = \"\\u{001B}[0;93m\"\n    static let BLUE_BRIGHT = \"\\u{001B}[0;94m\"\n    static let MAGENTA_BRIGHT = \"\\u{001B}[0;95m\"\n    static let CYAN_BRIGHT = \"\\u{001B}[0;96m\"\n    static let END = \"\\u{001B}[0;0m\"\n}\n\nlet PACKAGE_SWIFT_FILENAME = \"Package.swift\"\nlet XCODE_PROJECT_PATH_EXTENSION = \"xcodeproj\"\nlet XCODE_WORKSPACE_PATH_EXTENSION = \"xcworkspace\"\n\nenum RequiredReasonKey: CaseIterable, Comparable {\n    case FILE_TIMESTAMP_APIS_KEY\n    case SYSTEM_BOOT_APIS_KEY\n    case DISK_SPACE_APIS_KEY\n    case ACTIVE_KEYBOARD_APIS_KEY\n    case USER_DEFAULTS_APIS_KEY\n    case CORELOCATION_FRAMEWORK_KEY\n    case HEALTHKIT_FRAMEWORK_KEY\n    case CRASH_FRAMEWORK_KEY\n    case CONTACTS_FRAMEWORK_KEY\n    case THIRD_PARTY_SDK_KEY\n    \n    var description: String {\n        switch self {\n        case .FILE_TIMESTAMP_APIS_KEY:\n            return \"File Timestamp APIs\"\n        case .SYSTEM_BOOT_APIS_KEY:\n            return \"System boot time APIs\"\n        case .DISK_SPACE_APIS_KEY:\n            return \"Disk space APIs\"\n        case .ACTIVE_KEYBOARD_APIS_KEY:\n            return \"Active keyboard APIs\"\n        case .USER_DEFAULTS_APIS_KEY:\n            return \"User defaults APIs\"\n        case .CORELOCATION_FRAMEWORK_KEY:\n            return \"Core Location\"\n        case .HEALTHKIT_FRAMEWORK_KEY:\n            return \"HealthKit\"\n        case .CRASH_FRAMEWORK_KEY:\n            return \"Crash data\"\n        case .CONTACTS_FRAMEWORK_KEY:\n            return \"Contacts\"\n        case .THIRD_PARTY_SDK_KEY:\n            return \"Third-party SDKs\"\n        }\n    }\n\n    var privacyManifestKey: String? {\n        switch self {\n        case .FILE_TIMESTAMP_APIS_KEY:\n            return \"NSPrivacyAccessedAPICategoryFileTimestamp\"\n        case .SYSTEM_BOOT_APIS_KEY:\n            return \"NSPrivacyAccessedAPICategorySystemBootTime\"\n        case .DISK_SPACE_APIS_KEY:\n            return \"NSPrivacyAccessedAPICategoryDiskSpace\"\n        case .ACTIVE_KEYBOARD_APIS_KEY:\n            return \"NSPrivacyAccessedAPICategoryActiveKeyboards\"\n        case .USER_DEFAULTS_APIS_KEY:\n            return \"NSPrivacyAccessedAPICategoryUserDefaults\"\n        default:\n            return nil\n        }\n    }\n\n    var reasons: [String: String] {\n        switch self {\n        case .DISK_SPACE_APIS_KEY:\n            return [\n                \"85F4.1\" : \"\"\"\nDeclare this reason to display disk space information to the\nperson using the device.\nDisk space may be displayed in units of information (such as bytes)\nor units of time combined with a media type (such as minutes of HD video).\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device. There is an exception that allows the app to\nsend disk space information over the local network to another device\noperated by the same person only for the purpose of displaying disk space\ninformation on that device; this exception only applies if the user has\nprovided explicit permission to send disk space information,\nand the information may not be sent over the Internet.\n\"\"\",\n                \"E174.1\" : \"\"\"\nDeclare this reason to check whether there is sufficient disk space to\nwrite files, or to check whether the disk space is low so that the app can\ndelete files when the disk space is low. The app must behave differently\nbased on disk space in a way that is observable to users.\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device. There is an exception that allows the app to\navoid downloading files from a server when disk space is insufficient.\n\"\"\",\n                \"7D9E.1\" : \"\"\"\nDeclare this reason to include disk space information in an optional bug\nreport that the person using the device chooses to submit.\nThe disk space information must be prominently displayed to the person as\npart of the report.\n\nInformation accessed for this reason, or any derived information,\nmay be sent off-device only after the user affirmatively chooses to submit\nthe specific bug report including disk space information,\nand only for the purpose of investigating or responding to the bug report.\n\"\"\",\n                \"B728.1\" : \"\"\"\nDeclare this reason if your app is a health research app, and you access\nthis API category to detect and inform research participants about low disk\nspace impacting the research data collection.\n\nYour app must comply with App Store Review Guideline §5.1.3.\nYour app must not offer any functionality other than providing information\nabout and allowing people to participate in health research.\n\"\"\"\n            ]\n        case .FILE_TIMESTAMP_APIS_KEY:\n            return [\n                \"DDA9.1\" : \"\"\"\nDeclare this reason to display file timestamps to the person using the device.\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device.\n\"\"\",\n                \"C617.1\" : \"\"\"\nDeclare this reason to access the timestamps, size, or other metadata of files\ninside the app container, app group container, or the app’s CloudKit container.\n\"\"\",\n                \"3B52.1\" : \"\"\"\nDeclare this reason to access the timestamps, size, or other metadata of files\nor directories that the user specifically granted access to, such as using\na document picker view controller.\n\"\"\",\n                \"0A2A.1\" : \"\"\"\nDeclare this reason if your third-party SDK is providing a wrapper function\naround file timestamp API(s) for the app to use, and you only access the\nfile timestamp APIs when the app calls your wrapper function. This reason may\nonly be declared by third-party SDKs. This reason may not be declared if your\nthird-party SDK was created primarily to wrap required reason API(s).\n\nInformation accessed for this reason, or any derived information, \nmay not be used for your third-party SDK’s own purposes or sent off-device\nby your third-party SDK.\n\"\"\"\n            ]\n        case .SYSTEM_BOOT_APIS_KEY:\n            return [\n                \"35F9.1\" : \"\"\"\nDeclare this reason to access the system boot time in order to measure the\namount of time that has elapsed between events that occurred within the app\nor to perform calculations to enable timers.\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device. There is an exception for information about the\namount of time that has elapsed between events that occurred within the app,\nwhich may be sent off-device.\n\"\"\",\n                \"8FFB.1\" : \"\"\"\nDeclare this reason to access the system boot time to calculate absolute\ntimestamps for events that occurred within your app, such as events related\nto the UIKit or AVFAudio frameworks.\n\nAbsolute timestamps for events that occurred within your app may be sent\noff-device. System boot time accessed for this reason, or any other information\nderived from system boot time, may not be sent off-device.\n\"\"\",\n                \"3D61.1\" : \"\"\"\nDeclare this reason to include system boot time information in an optional\nbug report that the person using the device chooses to submit.\nThe system boot time information must be prominently displayed to the person\nas part of the report.\n\nInformation accessed for this reason, or any derived information,\nmay be sent off-device only after the user affirmatively chooses to submit\nthe specific bug report including system boot time information,\nand only for the purpose of investigating or responding to the bug report.\n\"\"\"\n            ]\n        case .ACTIVE_KEYBOARD_APIS_KEY:\n            return [\n                \"3EC4.1\" : \"\"\"\nDeclare this reason if your app is a custom keyboard app, and you access\nthis API category to determine the keyboards that are active on the device.\n\nProviding a systemwide custom keyboard to the user must be the primary\nfunctionality of the app.\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device.\n\"\"\",\n                \"54BD.1\" : \"\"\"\nDeclare this reason to access active keyboard information to present\nthe correct customized user interface to the person using the device.\nThe app must have text fields for entering or editing text and must behave\ndifferently based on active keyboards in a way that is observable to users.\n\nInformation accessed for this reason, or any derived information,\nmay not be sent off-device.\n\"\"\"\n            ]\n        case .USER_DEFAULTS_APIS_KEY:\n            return [\n                \"CA92.1\" : \"\"\"\nDeclare this reason to access user defaults to read and write information\nthat is only accessible to the app itself.\n\nThis reason does not permit reading information that was written by\nother apps or the system, or writing information that can be accessed by\nother apps.\n\"\"\",\n                \"1C8F.1\" : \"\"\"\nDeclare this reason to access user defaults to read and write information\nthat is only accessible to the apps, app extensions, and App Clips that\nare members of the same App Group as the app itself.\n\nThis reason does not permit reading information that was written by apps,\napp extensions, or App Clips outside the same App Group or by the system.\nYour app is not responsible if the system provides information from the\nglobal domain because a key is not present in your requested domain while\nyour app is attempting to read information that apps, app extensions,\nor App Clips in your app’s App Group write.\n\nThis reason also does not permit writing information that can be accessed\nby apps, app extensions, or App Clips outside the same App Group.\n\"\"\",\n                \"C56D.1\" : \"\"\"\nDeclare this reason if your third-party SDK is providing a wrapper function\naround user defaults API(s) for the app to use, and you only access the\nuser defaults APIs when the app calls your wrapper function.\nThis reason may only be declared by third-party SDKs.\nThis reason may not be declared if your third-party SDK was created primarily\nto wrap required reason API(s).\n\nInformation accessed for this reason, or any derived information,\nmay not be used for your third-party SDK’s own purposes or sent off-device\nby your third-party SDK.\n\"\"\",\n                \"AC6B.1\" : \"\"\"\nDeclare this reason to access user defaults to read the\ncom.apple.configuration.managed key to retrieve the managed app configuration\nset by MDM, or to set the com.apple.feedback.managed key to store\nfeedback information to be queried over MDM, as described in the\nApple Mobile Device Management Protocol Reference documentation.\n\"\"\"\n            ]\n        case .CORELOCATION_FRAMEWORK_KEY:\n            return [:] // TODO\n        case .HEALTHKIT_FRAMEWORK_KEY:\n            return [:] // TODO\n        case .CRASH_FRAMEWORK_KEY:\n            return [:] // TODO\n        case .CONTACTS_FRAMEWORK_KEY:\n            return [:] // TODO\n        case .THIRD_PARTY_SDK_KEY:\n            return [\n                \"reason\" : \"\"\"\n\\(CliSyntaxColor.BLUE_BRIGHT)\nℹ---------------------------------------------------------------------------------+\n| You must include the privacy manifest for any of the above SDKs when you submit |\n| new apps in App Store Connect that include those SDKs, or when you submit an    |\n| app update that adds one of the listed SDKs as part of the update.              |\n| Signatures are also required in these cases where the listed SDKs are used      |\n| as binary dependencies. Any version of a listed SDK, as well as any SDKs that   |\n| repackage those on the list, are included in the requirement.                   |\n+---------------------------------------------------------------------------------+\n\\(CliSyntaxColor.END)\n\"\"\"]\n        }\n    }\n\n    var link: String {\n        switch self {\n        case .FILE_TIMESTAMP_APIS_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393\"\n        case .SYSTEM_BOOT_APIS_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394\"\n        case .DISK_SPACE_APIS_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397\"\n        case .ACTIVE_KEYBOARD_APIS_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400\"\n        case .USER_DEFAULTS_APIS_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401\"\n        case .CORELOCATION_FRAMEWORK_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263133\"\n        case .HEALTHKIT_FRAMEWORK_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263132\"\n        case .CRASH_FRAMEWORK_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263159\"\n        case .CONTACTS_FRAMEWORK_KEY:\n            return \"https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263130\"\n        case .THIRD_PARTY_SDK_KEY:\n            return \"https://developer.apple.com/support/third-party-SDK-requirements/\"\n        }\n    }\n}\n\nlet ALLOWED_EXTENSIONS = [\n    \"m\",    // Objective-C\n    \"mm\",   // Objective-C++\n    \"c\",    // C\n    \"cpp\",  // C++\n    \"swift\" // Swift\n]\n\n// Look through the code for the listed strings (Case Sensitive)\nlet APIS_TO_CHECK: [String: [RequiredReasonKey]] = [\n    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api\n    \"creationDate\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"modificationDate\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"fileModificationDate\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"contentModificationDateKey\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"creationDateKey\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"getattrlist(\": [.FILE_TIMESTAMP_APIS_KEY, .DISK_SPACE_APIS_KEY], // also covers: fgetattrlist(\n    \"getattrlistbulk(\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"fstat(\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"fstatat(\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"lstat(\": [.FILE_TIMESTAMP_APIS_KEY],\n    \"getattrlistat(\": [.FILE_TIMESTAMP_APIS_KEY, .DISK_SPACE_APIS_KEY],\n    \"systemUptime\": [.SYSTEM_BOOT_APIS_KEY],\n    \"mach_absolute_time(\": [.SYSTEM_BOOT_APIS_KEY],\n\n    \"volumeAvailableCapacityKey\": [.DISK_SPACE_APIS_KEY],\n    \"volumeAvailableCapacityForImportantUsageKey\": [.DISK_SPACE_APIS_KEY],\n    \"volumeAvailableCapacityForOpportunisticUsageKey\": [.DISK_SPACE_APIS_KEY],\n    \"volumeTotalCapacityKey\": [.DISK_SPACE_APIS_KEY],\n    \"systemFreeSize\": [.DISK_SPACE_APIS_KEY],\n    \"systemSize\": [.DISK_SPACE_APIS_KEY],\n    \"statfs(\": [.DISK_SPACE_APIS_KEY], // also covers: fstatfs(\n    \"statvfs(\": [.DISK_SPACE_APIS_KEY], // also covers: fstatvfs(\n\n    \"activeInputModes\": [.ACTIVE_KEYBOARD_APIS_KEY],\n\n    \"UserDefaults\": [.USER_DEFAULTS_APIS_KEY],\n\n    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests\n    \"import CoreLocation\": [.CORELOCATION_FRAMEWORK_KEY],\n    \"#import <CoreLocation/CoreLocation.h>\": [.CORELOCATION_FRAMEWORK_KEY],\n\n    \"import HealthKit\": [.HEALTHKIT_FRAMEWORK_KEY],\n    \"#import <HealthKit/HealthKit.h>\": [.HEALTHKIT_FRAMEWORK_KEY],\n    \"#import <HealthKitUI/HealthKitUI.h>\": [.HEALTHKIT_FRAMEWORK_KEY],\n\n    \"import Sentry\": [.CRASH_FRAMEWORK_KEY],\n    \"#import <Sentry/Sentry.h>\": [.CRASH_FRAMEWORK_KEY],\n    \"import Instabug\": [.CRASH_FRAMEWORK_KEY],\n    \"#import <Instabug/Instabug.h>\": [.CRASH_FRAMEWORK_KEY],\n    \"#import \\\"Countly.h\\\"\": [.CRASH_FRAMEWORK_KEY],\n    \"import Bugsnag\": [.CRASH_FRAMEWORK_KEY],\n    \"#import <Bugsnag/Bugsnag.h>\": [.CRASH_FRAMEWORK_KEY],\n    \"#import <Firebase\": [.CRASH_FRAMEWORK_KEY],\n    \"import Firebase\": [.CRASH_FRAMEWORK_KEY],\n    // TODO: Add more third-party crash frameworks here\n\n    \"import Contacts\": [.CONTACTS_FRAMEWORK_KEY],\n    \"#import <ContactsUI/ContactsUI.h>\": [.CONTACTS_FRAMEWORK_KEY],\n    \"#import <Contacts/Contacts.h>\": [.CONTACTS_FRAMEWORK_KEY]\n]\n\n// Look through the Frameworks Build Phase or Package Dependencies for the\n// listed strings (Case Insensitive)\nlet SDKS_TO_CHECK: [String: RequiredReasonKey] = [\n    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests\n    \"sentry-cocoa\": .CRASH_FRAMEWORK_KEY,\n    \"Sentry\": .CRASH_FRAMEWORK_KEY,\n    \"Instabug\": .CRASH_FRAMEWORK_KEY,\n    \"countly\": .CRASH_FRAMEWORK_KEY,\n    \"Bugsnag\": .CRASH_FRAMEWORK_KEY,\n    \"firebase\": .CRASH_FRAMEWORK_KEY,\n    // TODO: Add more third-party crash frameworks here\n\n    \"CoreLocation\": .CORELOCATION_FRAMEWORK_KEY,\n    \"HealthKit\": .HEALTHKIT_FRAMEWORK_KEY,\n    \"Contacts\": .CONTACTS_FRAMEWORK_KEY, // also covers ContactsUI\n\n    // Ref: https://developer.apple.com/support/third-party-SDK-requirements/\n    \"Abseil\": .THIRD_PARTY_SDK_KEY,\n    \"AFNetworking\": .THIRD_PARTY_SDK_KEY,\n    \"Alamofire\": .THIRD_PARTY_SDK_KEY,\n    \"AppAuth\": .THIRD_PARTY_SDK_KEY, // also covers: GTMAppAuth\n    \"BoringSSL\": .THIRD_PARTY_SDK_KEY,\n    \"openssl_grpc\": .THIRD_PARTY_SDK_KEY,\n    \"Capacitor\": .THIRD_PARTY_SDK_KEY,\n    \"Charts\": .THIRD_PARTY_SDK_KEY,\n    \"connectivity_plus\": .THIRD_PARTY_SDK_KEY,\n    \"Cordova\": .THIRD_PARTY_SDK_KEY,\n    \"device_info_plus\": .THIRD_PARTY_SDK_KEY,\n    \"DKImagePickerController\": .THIRD_PARTY_SDK_KEY,\n    \"DKPhotoGallery\": .THIRD_PARTY_SDK_KEY,\n    \"FBAEMKit\": .THIRD_PARTY_SDK_KEY,\n    \"FBLPromises\": .THIRD_PARTY_SDK_KEY,\n    \"FBSDKCoreKit\": .THIRD_PARTY_SDK_KEY, // also covers: FBSDKCoreKit_Basics\n    \"FBSDKLoginKit\": .THIRD_PARTY_SDK_KEY,\n    \"FBSDKShareKit\": .THIRD_PARTY_SDK_KEY,\n    \"file_picker\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseABTesting\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseAuth\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseCore\": .THIRD_PARTY_SDK_KEY, // also covers: FirebaseCoreDiagnostics, FirebaseCoreExtension, FirebaseCoreInternal\n    \"FirebaseCrashlytics\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseDynamicLinks\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseFirestore\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseInstallations\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseMessaging\": .THIRD_PARTY_SDK_KEY,\n    \"FirebaseRemoteConfig\": .THIRD_PARTY_SDK_KEY,\n    \"Flutter\": .THIRD_PARTY_SDK_KEY, // also covers: flutter_inappwebview, flutter_local_notifications, fluttertoast\n    \"FMDB\": .THIRD_PARTY_SDK_KEY,\n    \"geolocator_apple\": .THIRD_PARTY_SDK_KEY,\n    \"GoogleDataTransport\": .THIRD_PARTY_SDK_KEY,\n    \"GoogleSignIn\": .THIRD_PARTY_SDK_KEY,\n    \"GoogleToolboxForMac\": .THIRD_PARTY_SDK_KEY,\n    \"GoogleUtilities\": .THIRD_PARTY_SDK_KEY,\n    \"grpcpp\": .THIRD_PARTY_SDK_KEY,\n    \"GTMSessionFetcher\": .THIRD_PARTY_SDK_KEY,\n    \"hermes\": .THIRD_PARTY_SDK_KEY,\n    \"image_picker_ios\": .THIRD_PARTY_SDK_KEY,\n    \"IQKeyboardManager\": .THIRD_PARTY_SDK_KEY, // also covers: IQKeyboardManagerSwift\n    \"Kingfisher\": .THIRD_PARTY_SDK_KEY,\n    \"leveldb\": .THIRD_PARTY_SDK_KEY,\n    \"Lottie\": .THIRD_PARTY_SDK_KEY,\n    \"MBProgressHUD\": .THIRD_PARTY_SDK_KEY,\n    \"nanopb\": .THIRD_PARTY_SDK_KEY,\n    \"OneSignal\": .THIRD_PARTY_SDK_KEY, // also covers: OneSignalCore, OneSignalExtension, OneSignalOutcomes\n    \"OpenSSL\": .THIRD_PARTY_SDK_KEY,\n    \"OrderedSet\": .THIRD_PARTY_SDK_KEY,\n    \"package_info\": .THIRD_PARTY_SDK_KEY, // also covers: package_info_plus\n    \"path_provider\": .THIRD_PARTY_SDK_KEY, // also covers: path_provider_ios\n    \"Promises\": .THIRD_PARTY_SDK_KEY,\n    \"Protobuf\": .THIRD_PARTY_SDK_KEY,\n    \"Reachability\": .THIRD_PARTY_SDK_KEY,\n    \"RealmSwift\": .THIRD_PARTY_SDK_KEY,\n    \"RxCocoa\": .THIRD_PARTY_SDK_KEY,\n    \"RxRelay\": .THIRD_PARTY_SDK_KEY,\n    \"RxSwift\": .THIRD_PARTY_SDK_KEY,\n    \"SDWebImage\": .THIRD_PARTY_SDK_KEY,\n    \"share_plus\": .THIRD_PARTY_SDK_KEY,\n    \"shared_preferences_ios\": .THIRD_PARTY_SDK_KEY,\n    \"SnapKit\": .THIRD_PARTY_SDK_KEY,\n    \"sqflite\": .THIRD_PARTY_SDK_KEY,\n    \"Starscream\": .THIRD_PARTY_SDK_KEY,\n    \"SVProgressHUD\": .THIRD_PARTY_SDK_KEY,\n    \"SwiftyGif\": .THIRD_PARTY_SDK_KEY,\n    \"SwiftyJSON\": .THIRD_PARTY_SDK_KEY,\n    \"Toast\": .THIRD_PARTY_SDK_KEY,\n    \"UnityFramework\": .THIRD_PARTY_SDK_KEY,\n    \"url_launcher\": .THIRD_PARTY_SDK_KEY, // also covers: url_launcher_ios\n    \"video_player_avfoundation\": .THIRD_PARTY_SDK_KEY,\n    \"wakelock\": .THIRD_PARTY_SDK_KEY,\n    \"webview_flutter_wkwebview\": .THIRD_PARTY_SDK_KEY\n]\n"
  },
  {
    "path": "Sources/PrivacyManifest/DirectoryProjectParser.swift",
    "content": "//\n//  DirectoryProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 15/4/24.\n//\n\nimport Foundation\n\nimport PathKit\n\n// Recursively rarses all the children files of the provided path, detects the\n// supported files and parses them.\nclass DirectoryProjectParser: ProjectParser {\n    override func parse() throws {\n        print(\"---\")\n\n        let targetName = projectPath.lastComponent\n        let spinner = concurrentStream.createSilentSpinner(with: \"Parsing \\(CliSyntaxColor.GREEN)\\(targetName)'s\\(CliSyntaxColor.END) source files...\")\n        concurrentStream.start(spinner: spinner)\n        do {\n            var filePathsForParsing: [Path] = []\n            try self.projectPath.recursiveChildren().forEach { path in\n                guard let ext = path.extension,\n                      ALLOWED_EXTENSIONS.contains(ext) else {\n                    return\n                }\n                filePathsForParsing.append(path)\n            }\n            try self.parseFiles(filePathsForParsing: filePathsForParsing,\n                                targetName: targetName,\n                                spinner: spinner)\n            self.concurrentStream.success(spinner: spinner,\n                                          \"Parsed \\(CliSyntaxColor.GREEN)\\(targetName)'s\\(CliSyntaxColor.END) source files\")\n        }\n        catch {\n            self.concurrentStream.error(spinner: spinner,\n                                        \"Error parsing \\(CliSyntaxColor.GREEN)\\(targetName)'s\\(CliSyntaxColor.END) source files: \\(CliSyntaxColor.RED)\\(error)\\(CliSyntaxColor.END)\")\n        }\n\n        _ = group.wait(timeout: .distantFuture)\n        concurrentStream.waitAndShowCursor()\n    }\n}\n"
  },
  {
    "path": "Sources/PrivacyManifest/ProjectParser.swift",
    "content": "//\n//  ProjectParser.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\nimport PathKit\n\nstruct ParsedResult: Hashable {\n    var line: String\n    var lineNumber: Int?\n    var range: Range<String.Index>\n}\n\nstruct PresentedResult: Hashable {\n    var filePath: String\n    var formattedLine: String?\n    var parsedResult: ParsedResult?\n}\n\nclass ProjectParser {\n    private var requiredAPIs: [RequiredReasonKey: Set<PresentedResult>] = [:]\n    private let requiredAPIsLock = NSLock()\n\n    let concurrentStream = ConcurrentSpinnerStream()\n    let group = DispatchGroup()\n    let queue = DispatchQueue(label: \"parser\",\n                              attributes: .concurrent)\n\n    let projectPath: Path\n\n    init(with projectPath: Path) {\n        self.projectPath = projectPath\n        RequiredReasonKey.allCases.forEach { key in\n            requiredAPIs[key] = Set()\n        }\n    }\n\n    func parse() throws { }\n\n    final func parseFiles(filePathsForParsing: [Path],\n                          targetName: String,\n                          spinner: Spinner) throws {\n        guard filePathsForParsing.count > 0 else {\n            return\n        }\n        var parsed = 1\n        for filePath in filePathsForParsing {\n            self.queue.async(group: group,\n                             execute: DispatchWorkItem(block: {\n                defer {\n                    parsed += 1\n                    self.concurrentStream.message(spinner: spinner,\n                                             \"Parsing \\(CliSyntaxColor.GREEN)\\(targetName)'s\\(CliSyntaxColor.END) source files (\\(parsed)/\\(filePathsForParsing.count))...\")\n                }\n                guard let fileHandle = FileHandle(forReadingAtPath: filePath.string) else {\n                    return\n                }\n                do {\n                    guard let data = try fileHandle.readToEnd(),\n                          let contents = String(data: data, encoding: .utf8) else {\n                        return\n                    }\n\n                    Self.lookForAPI(contents: contents).forEach { (key, parsedResult) in\n                        let highlightedCode = \"\\(Self.addBracketsToString(parsedResult.line,around: parsedResult.range))\"\n                        var formattedLine = \"\"\n                        if let lineNumber = parsedResult.lineNumber {\n                            formattedLine = \"\\(CliSyntaxColor.GREEN)\\(lineNumber):\\(CliSyntaxColor.END)\\t\\(highlightedCode)\"\n                        }\n                        else {\n                            formattedLine = \"\\(highlightedCode)\"\n                        }\n                        self.updateRequiredAPIs(key,\n                                                with: PresentedResult(filePath: filePath.string,\n                                                                      formattedLine: formattedLine,\n                                                                      parsedResult: parsedResult))\n                    }\n                }\n                catch {\n                    self.concurrentStream.error(spinner: spinner, \"Error: \\(error)\")\n                }\n            }))\n        }\n    }\n\n    final func updateRequiredAPIs(_ key: RequiredReasonKey,\n                                  with presentedResult: PresentedResult) {\n        self.requiredAPIsLock.lock()\n        self.requiredAPIs[key]?.update(with: presentedResult)\n        self.requiredAPIsLock.unlock()\n    }\n\n    final func process(revealOccurrences: Bool) -> [RequiredReasonKey: Set<PresentedResult>] {\n        print(\"---\")\n        requiredAPIs.sorted(by: {\n            if $0.value.count == $1.value.count {\n                $0.key.hashValue < $1.key.hashValue\n            }\n            else {\n                $0.value.count < $1.value.count\n            }\n        }).forEach { (key, list) in\n            print(\"\\(CliSyntaxColor.WHITE_BOLD)\\(key.description) (\\(list.count) \\(list.count == 1 ? \"occurrence\" : \"occurrences\")\\(CliSyntaxColor.END))\")\n\n            if !revealOccurrences {\n                return\n            }\n            var currentPath = \"\"\n            list.sorted(by: {\n                if $0.parsedResult == nil && $1.parsedResult != nil {\n                    return true\n                } else if $0.parsedResult != nil && $1.parsedResult == nil {\n                    return false\n                }\n                else if let firstParsedResult = $0.parsedResult,\n                         let secondParentResult = $1.parsedResult {\n                    if $0.filePath == $1.filePath {\n                        return firstParsedResult.lineNumber ?? -1 < secondParentResult.lineNumber ?? -1\n                    }\n                    else {\n                        return $0.filePath < $1.filePath\n                    }\n                }\n                return $0.filePath < $1.filePath\n            }).forEach { presentedResult in\n                if presentedResult.filePath != currentPath {\n                    print(\"\\n\\t\\(presentedResult.formattedLine != nil ? \"✎\" : \"⛺︎\") \\(presentedResult.filePath)\\(presentedResult.formattedLine != nil ? \":\" : \"\")\")\n                }\n\n                if let formattedLine = presentedResult.formattedLine {\n                    print(\"\\t\\t\\(formattedLine)\")\n                }\n\n                currentPath = presentedResult.filePath\n            }\n\n            print(\"\\n\")\n        }\n\n        return requiredAPIs\n    }\n}\n\n// Helper methods\nextension ProjectParser {\n    static func lookForAPI(contents: String) -> [(RequiredReasonKey, ParsedResult)] {\n        var foundAPIs: [(RequiredReasonKey, ParsedResult)] = []\n        var lineNumber = 1\n        contents.components(separatedBy: .newlines).forEach { line in\n            APIS_TO_CHECK.forEach { (api, requiredReasonKeys) in\n                let results = mark(searchString: api,\n                                   in: line,\n                                   lineNumber: lineNumber,\n                                   requiredReasonKeys: requiredReasonKeys)\n                foundAPIs.append(contentsOf: results)\n            }\n            lineNumber += 1\n        }\n        return foundAPIs\n    }\n\n    static func mark(searchString: String,\n                     in line: String,\n                     lineNumber: Int?,\n                     caseInsensitive: Bool = false,\n                     requiredReasonKeys: [RequiredReasonKey]) -> [(RequiredReasonKey, ParsedResult)] {\n        var parsedResults: [(RequiredReasonKey, ParsedResult)] = []\n        var searchRange = line.startIndex..<line.endIndex\n        while let range = line.range(of: searchString,\n                                     options: caseInsensitive ? .caseInsensitive : [],\n                                     range: searchRange) {\n            requiredReasonKeys.forEach { requiredReasonKey in\n                parsedResults.append((requiredReasonKey, ParsedResult(line: line,\n                                                                      lineNumber: lineNumber,\n                                                                      range: range)))\n            }\n            searchRange = range.upperBound..<line.endIndex\n        }\n        return parsedResults\n    }\n\n    static func addTagsToString(_ string: String,\n                                around range: Range<String.Index>,\n                                openingTag: String,\n                                closingTag: String) -> String {\n        let lowerBoundIndex = range.lowerBound\n        let upperBoundIndex = range.upperBound\n        var modifiedString = string\n        modifiedString.replaceSubrange(lowerBoundIndex..<lowerBoundIndex,\n                                       with: openingTag)\n        let adjustedUpperBoundIndex = modifiedString.index(upperBoundIndex,\n                                                           offsetBy: openingTag.count)\n        modifiedString.replaceSubrange(adjustedUpperBoundIndex..<adjustedUpperBoundIndex,\n                                       with: closingTag)\n        return modifiedString\n    }\n\n    static func addBracketsToString(_ string: String,\n                                    around range: Range<String.Index>) -> String {\n        return addTagsToString(string, around: range,\n                               openingTag: CliSyntaxColor.YELLOW,\n                               closingTag: CliSyntaxColor.END)\n    }\n}\n"
  },
  {
    "path": "Sources/PrivacyManifest/SpinnerStreams.swift",
    "content": "//\n//  SpinnerStreams.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\n\n// Displays several different spinner outputs at once\nclass ConcurrentSpinnerStream {\n    // The array of concurrent silent spinner streams to manage\n    var silentSpinnerStreams: [SilentSpinnerStream] = []\n\n    private static let MAX_OUTPUT_LINES = 10\n\n    private var previousRows = 0\n\n    // Serial queue that ensures that console output is serial.\n    private let queue = DispatchQueue(label: \"stream.queue\")\n    private let group = DispatchGroup()\n\n    // Serial queue that ensures that there are no race conditions on spinner\n    // calls.\n    private let spinnersQueue = DispatchQueue(label: \"spinner.queue\")\n    private let spinnersGroup = DispatchGroup()\n\n    init() {\n        Self.hideCursor()\n    }\n\n    // Renders the added silent spinner streams\n    // NOTE: Only call it from within the serial qeueue\n    private func render() {\n        guard silentSpinnerStreams.count > 0 else {\n            return\n        }\n\n        // Move cursor at the beginning of the previously rendered string\n        if previousRows > 0 {\n            print(\"\\u{001B}[\\(previousRows)F\", terminator: \"\")\n        }\n        // Clear from cursor to end of screen\n        print(\"\\u{001B}[0J\", terminator: \"\")\n\n        // Generate the buffer\n        var buffer = \"\"\n        var linesRendered = 0\n\n        silentSpinnerStreams.sorted().forEach { silentSpinner in\n            if linesRendered > Self.MAX_OUTPUT_LINES {\n                return\n            }\n            buffer.append(silentSpinner.buffer + \"\\n\")\n            linesRendered += 1\n        }\n\n        print(\"\\(buffer)\", terminator: \"\")\n        fflush(stdout)\n\n        previousRows = linesRendered\n    }\n\n    // Hides the cursor from console\n    static func hideCursor() {\n        print(\"\\u{001B}[?25l\", terminator: \"\")\n        fflush(stdout)\n    }\n\n    // Shows the cursor to console\n    static func showCursor() {\n        print(\"\\u{001B}[?25h\", terminator: \"\")\n        fflush(stdout)\n    }\n\n    func waitAndShowCursor() {\n        // Wait until all async requests have been printed\n        _ = spinnersGroup.wait(timeout: .distantFuture)\n        _ = group.wait(timeout: .distantFuture)\n\n        Self.showCursor()\n\n        silentSpinnerStreams.removeAll()\n        previousRows = 0\n    }\n\n    // Adds a silent spinner stream\n    func add(stream: SilentSpinnerStream) {\n        queue.async(group: group,\n                    execute: DispatchWorkItem(block: {\n            self.silentSpinnerStreams.append(stream)\n        }))\n    }\n\n    /// Execute an asynchronous task on the serial queue and optionally render the added silent spinner\n    /// streams.\n    ///\n    /// - Parameters:\n    ///   - work: The task to be completed asynchronously within the serial queue\n    private func executeSpinnerAsync(work: @escaping () -> Void) {\n        spinnersQueue.async(group: spinnersGroup,\n                            execute: DispatchWorkItem(block: {\n            work()\n        }))\n    }\n\n    fileprivate func executeSpinnerStreamAsync(work: @escaping () -> Void) {\n        queue.async(group: group,\n                    execute: DispatchWorkItem(block: {\n            work()\n            self.render()\n        }))\n    }\n\n    func start(spinner: Spinner) {\n        executeSpinnerAsync {\n            spinner.start()\n        }\n    }\n\n    func success(spinner: Spinner, _ message: String) {\n        executeSpinnerAsync {\n            spinner.success(message)\n        }\n    }\n\n    func message(spinner: Spinner, _ message: String) {\n        executeSpinnerAsync {\n            spinner.message(message)\n        }\n    }\n\n    func error(spinner: Spinner, _ message: String) {\n        executeSpinnerAsync {\n            spinner.error(message)\n        }\n    }\n\n    func createSilentSpinner(with message: String) -> Spinner {\n        let silentSpinnerStream = SilentSpinnerStream(concurrentStream: self)\n        return Spinner(.dots8Bit, message,\n                       stream: silentSpinnerStream)\n    }\n}\n\n// Writes the spinner stream to a buffer, instead of the stdout\nclass SilentSpinnerStream: SpinnerStream, Comparable {\n    static func == (lhs: SilentSpinnerStream, rhs: SilentSpinnerStream) -> Bool {\n        lhs.lastUpdated == rhs.lastUpdated\n    }\n    \n    static func < (lhs: SilentSpinnerStream, rhs: SilentSpinnerStream) -> Bool {\n        lhs.lastUpdated > rhs.lastUpdated\n    }\n    \n    var buffer = \"\"\n    var lastUpdated: TimeInterval\n\n    private var concurrentStream: ConcurrentSpinnerStream\n\n    init(concurrentStream: ConcurrentSpinnerStream) {\n        self.lastUpdated = Date().timeIntervalSince1970\n        self.concurrentStream = concurrentStream\n        concurrentStream.add(stream: self)\n    }\n\n    func write(string: String, terminator: String) {\n        guard string.count > 0 else {\n            return\n        }\n        concurrentStream.executeSpinnerStreamAsync(work: {\n            self.lastUpdated = Date().timeIntervalSince1970\n            // If the message contains a success or an error character, treat\n            // it as the final message for that spinner stream.\n            guard !self.buffer.contains(\"✔\") && !self.buffer.contains(\"✖\") else {\n                return\n            }\n            self.buffer = string\n        })\n    }\n\n    func hideCursor() { }\n\n    func showCursor() { }\n}\n"
  },
  {
    "path": "Sources/PrivacyManifest/SwiftPackageProjectParser.swift",
    "content": "//\n//  SwiftPackageProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\nimport PathKit\n\nimport PackageModel\nimport PackageLoading\nimport PackageGraph\nimport Workspace\nimport Basics\nimport class TSCBasic.Process\nimport func TSCBasic.tsc_await\n\nextension SwiftSDK {\n    package static var `default`: Self {\n        get throws {\n            // ref: https://github.com/compnerd/swift-package-manager/blob/master/Examples/package-info/Sources/package-info/main.swift#L10\n            let swiftCompiler: AbsolutePath? = {\n                let string: String\n                #if os(macOS)\n                string = try! Process.checkNonZeroExit(args: \"xcrun\", \"--sdk\", \"macosx\", \"-f\", \"swiftc\").spm_chomp()\n                #else\n                string = try! Process.checkNonZeroExit(args: \"which\", \"swiftc\").spm_chomp()\n                #endif\n                return try! AbsolutePath(validating: string)\n            }()\n            return try! SwiftSDK.hostSwiftSDK(swiftCompiler)\n        }\n    }\n}\n\nextension UserToolchain {\n    package static var `default`: Self {\n        get throws {\n            return try .init(swiftSDK: SwiftSDK.default)\n        }\n    }\n}\n\n// Parses all the Swift Package's supported source files and dependencies.\nclass SwiftPackageProjectParser : ProjectParser {\n    override func parse() throws {\n        print(\"---\")\n\n        let spinner = Spinner(.dots8Bit,\n                              \"Resolving graph...\")\n        spinner.start()\n        let manifestLoader = try ManifestLoader(toolchain: UserToolchain.default)\n        let packageAbsolutePath = try AbsolutePath(validating: projectPath.string)\n        let root = packageAbsolutePath.parentDirectory\n        // ref: https://github.com/unsignedapps/swift-create-xcframework/blob/0be3a68c84987493a7d7298027274a0862bc5ccd/Sources/CreateXCFramework/PackageInfo.swift#L93\n        let workspace = try Workspace(\n            forRootPackage: root,\n            customManifestLoader: manifestLoader\n        )\n        // Only print warning and error messages\n        let observability = Basics.ObservabilitySystem { _, diagnostics in\n            guard diagnostics.severity != .debug && diagnostics.severity != .info else {\n                return\n            }\n            print(\"\\(diagnostics.severity): \\(diagnostics.message)\")\n        }\n        let scope = observability.topScope\n        let graph = try workspace.loadPackageGraph(\n            rootPath: root,\n            observabilityScope: scope\n        )\n        spinner.success(\"Resolved graph\")\n\n        graph.requiredDependencies.forEach { dependency in\n            guard !dependency.kind.isRoot else {\n                return\n            }\n\n            let dependencyString = dependency.canonicalLocation.description\n            let spinner = concurrentStream.createSilentSpinner(with: \"Parsing \\(CliSyntaxColor.GREEN)\\(dependencyString)\\(CliSyntaxColor.END) dependency...\")\n            concurrentStream.start(spinner: spinner)\n            queue.async(group: group,\n                        execute: DispatchWorkItem(block: {\n                SDKS_TO_CHECK.forEach { (key, value) in\n                    let markedResults = Self.mark(searchString: key,\n                                                  in: dependencyString,\n                                                  lineNumber: nil,\n                                                  caseInsensitive: true,\n                                                  requiredReasonKeys: [value])\n                    guard let firstResult = markedResults.first?.1 else {\n                        return\n                    }\n                    let highlightedCode = \"\\(Self.addBracketsToString(firstResult.line,around: firstResult.range))\"\n                    let foundInBuildPhase = \"Found \\(highlightedCode) in dependencies.\"\n                    self.updateRequiredAPIs(value,\n                                            with: PresentedResult(filePath: foundInBuildPhase))\n                }\n                self.concurrentStream.success(spinner: spinner,\n                                              \"Parsed \\(CliSyntaxColor.GREEN)\\(dependencyString)\\(CliSyntaxColor.END) dependency\")\n            }))\n        }\n\n        // We only care about the targets of the root packages, not the\n        // dependencies\n        graph.rootPackages.forEach { package in\n            package.targets.forEach { target in\n                // Exclude test targets\n                guard target.type != .test else {\n                    return\n                }\n\n                let spinner = concurrentStream.createSilentSpinner(with: \"Parsing \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files...\")\n                concurrentStream.start(spinner: spinner)\n                var filePathsForParsing: [Path] = []\n                let rootDirectory = target.sources.root\n                target.sources.relativePaths.forEach { relativePath in\n                    guard let ext = relativePath.extension,\n                          ALLOWED_EXTENSIONS.contains(ext) else {\n                        return\n                    }\n                    filePathsForParsing.append(Path(rootDirectory.pathString) + relativePath.pathString)\n                }\n                do {\n                    try self.parseFiles(filePathsForParsing: filePathsForParsing,\n                                        targetName: target.name,\n                                        spinner: spinner)\n                    self.concurrentStream.success(spinner: spinner,\n                                                  \"Parsed \\(filePathsForParsing.count) \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files\")\n                }\n                catch {\n                    self.concurrentStream.error(spinner: spinner,\n                                                \"Error parsing \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files: \\(CliSyntaxColor.RED)\\(error)\\(CliSyntaxColor.END)\")\n                }\n            }\n        }\n\n        _ = group.wait(timeout: .distantFuture)\n        concurrentStream.waitAndShowCursor()\n\n        print(\"---\")\n    }\n}\n"
  },
  {
    "path": "Sources/PrivacyManifest/XcodeProjectParser.swift",
    "content": "//\n//  XcodeProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\nimport PathKit\n\nimport XcodeProj\n\n// Parses all Xcode projects contained in a Xcode workspace\nclass XcodeWorkspaceParser: XcodeProjectParser {\n    override func parse() throws {\n        print(\"---\")\n\n        let xcworkspace = try XCWorkspace(path: projectPath)\n\n        try xcworkspace.data.children.filter {\n            Path($0.location.path).extension == XCODE_PROJECT_PATH_EXTENSION\n        }.forEach { element in\n            print(\"\\(CliSyntaxColor.WHITE_BOLD)\\(element.location.path)\\(CliSyntaxColor.END)\")\n\n            ConcurrentSpinnerStream.hideCursor()\n\n            let projectPath = projectPath.parent() + Path(element.location.path)\n\n            try parseProject(projectPath)\n        }\n    }\n}\n\n// Parses all targets' supported source files and frameworks.\nclass XcodeProjectParser: ProjectParser {\n    var deepLibraryFrameworkCheck = false\n\n    init(with path: Path,\n         deepLibraryFrameworkCheck: Bool = false) {\n        super.init(with: path)\n        self.deepLibraryFrameworkCheck = deepLibraryFrameworkCheck\n    }\n\n    override func parse() throws {\n        print(\"---\")\n\n        try parseProject(projectPath)\n    }\n\n    fileprivate func parseProject(_ path: Path) throws {\n        let xcodeproj = try XcodeProj(path: path)\n\n        // Gather all local and remote package dependencies from the root\n        // project.\n        var packageDepedencyNames: [String] = []\n        do {\n            try xcodeproj.pbxproj.rootProject()?.localPackages.compactMap {\n                $0.name\n            }.forEach {\n                packageDepedencyNames.append($0)\n            }\n            try xcodeproj.pbxproj.rootProject()?.remotePackages.compactMap {\n                $0.name\n            }.forEach {\n                packageDepedencyNames.append($0)\n            }\n        }\n        catch {\n            // Suppress any errors due to missing references.\n        }\n\n        packageDepedencyNames.forEach { packageDepedencyName in\n            let spinner = concurrentStream.createSilentSpinner(with: \"Looking up \\(CliSyntaxColor.GREEN)\\(packageDepedencyName)'s\\(CliSyntaxColor.END) package dependency...\")\n            concurrentStream.start(spinner: spinner)\n            queue.async(group: group,\n                        execute: DispatchWorkItem(block: {\n                SDKS_TO_CHECK.forEach { (key, value) in\n                    let markedResults = Self.mark(searchString: key,\n                                                  in: packageDepedencyName,\n                                                  lineNumber: nil,\n                                                  caseInsensitive: true,\n                                                  requiredReasonKeys: [value])\n                    guard let firstResult = markedResults.first?.1 else {\n                        return\n                    }\n                    let highlightedCode = \"\\(Self.addBracketsToString(firstResult.line,around: firstResult.range))\"\n                    let foundInBuildPhase = \"Found \\(highlightedCode) in Package Dependencies.\"\n                    self.updateRequiredAPIs(value,\n                                            with: PresentedResult(filePath: foundInBuildPhase))\n                }\n                self.concurrentStream.success(spinner: spinner,\n                                              \"Parsed \\(CliSyntaxColor.GREEN)\\(packageDepedencyName)\\(CliSyntaxColor.END) package dependency\")\n            }))\n        }\n\n        try xcodeproj.pbxproj.nativeTargets.forEach { target in\n            guard let productType = target.productType else {\n                return\n            }\n\n            // Skip UI / Unit tests\n            if productType == .unitTestBundle || productType == .uiTestBundle {\n                return\n            }\n\n            if productType == .staticLibrary\n                || productType == .staticFramework\n                || productType == .framework\n                || productType == .xcFramework {\n                let spinner = concurrentStream.createSilentSpinner(with: \"Looking up \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) name...\")\n                concurrentStream.start(spinner: spinner)\n                queue.async(group: group,\n                            execute: DispatchWorkItem(block: {\n                    SDKS_TO_CHECK.forEach { (key, value) in\n                        let markedResults = Self.mark(searchString: key,\n                                                      in: target.name,\n                                                      lineNumber: nil,\n                                                      caseInsensitive: true,\n                                                      requiredReasonKeys: [value])\n                        guard let firstResult = markedResults.first?.1 else {\n                            return\n                        }\n                        let highlightedCode = \"\\(Self.addBracketsToString(firstResult.line,around: firstResult.range))\"\n                        let foundInBuildPhase = \"Found \\(highlightedCode).\"\n                        self.updateRequiredAPIs(value,\n                                                with: PresentedResult(filePath: foundInBuildPhase))\n                    }\n                    self.concurrentStream.success(spinner: spinner,\n                                                  \"Looked up \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) name\")\n                }))\n                // Do not proceed into looking at the build phase or the source\n                // files of the project targets that are libraries or frameworks\n                // if not instructed.\n                if !deepLibraryFrameworkCheck {\n                    return\n                }\n            }\n\n            // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests\n            target.buildPhases.forEach { phase in\n                guard phase.buildPhase == .frameworks else {\n                    return\n                }\n                guard let files = phase.files, files.count > 0 else {\n                    return\n                }\n                let spinner = concurrentStream.createSilentSpinner(with: \"Parsing \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) Frameworks Build Phase...\")\n                concurrentStream.start(spinner: spinner)\n                queue.async(group: group,\n                            execute: DispatchWorkItem(block: {\n                    files.forEach({ file in\n                        guard let fullFileName = file.file?.name else {\n                            return\n                        }\n                        SDKS_TO_CHECK.forEach { (key, value) in\n                            let markedResults = Self.mark(searchString: key,\n                                                          in: fullFileName,\n                                                          lineNumber: nil,\n                                                          caseInsensitive: true,\n                                                          requiredReasonKeys: [value])\n                            guard let firstResult = markedResults.first?.1 else {\n                                return\n                            }\n                            let highlightedCode = \"\\(Self.addBracketsToString(firstResult.line,around: firstResult.range))\"\n                            let foundInBuildPhase = \"Found \\(highlightedCode) in \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) Frameworks Build Phase.\"\n                            self.updateRequiredAPIs(value,\n                                                    with: PresentedResult(filePath: foundInBuildPhase))\n                        }\n                    })\n                    self.concurrentStream.success(spinner: spinner,\n                                                  \"Parsed \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) Frameworks Build Phase\")\n                }))\n            }\n\n            var sourceFiles: [PBXFileElement] = []\n            do {\n                sourceFiles = try target.sourceFiles()\n            }\n            catch is PBXObjectError {\n                // Suppress PBXObjectError\n            }\n\n            let spinner = concurrentStream.createSilentSpinner(with: \"Parsing \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files...\")\n            concurrentStream.start(spinner: spinner)\n            do {\n                var filePathsForParsing: [Path] = []\n                try sourceFiles.forEach { file in\n                    guard let filePath = file.path,\n                          let ext = Path(filePath).extension,\n                          ALLOWED_EXTENSIONS.contains(ext)\n                    else {\n                        return\n                    }\n                    guard let fullPath = try file.fullPath(sourceRoot: path.parent()) else {\n                        return\n                    }\n                    filePathsForParsing.append(fullPath)\n                }\n                try self.parseFiles(filePathsForParsing: filePathsForParsing,\n                                    targetName: target.name,\n                                    spinner: spinner)\n                self.concurrentStream.success(spinner: spinner,\n                                              \"Parsed \\(filePathsForParsing.count) \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files\")\n            }\n            catch {\n                self.concurrentStream.error(spinner: spinner,\n                                            \"Error parsing \\(CliSyntaxColor.GREEN)\\(target.name)'s\\(CliSyntaxColor.END) source files: \\(CliSyntaxColor.RED)\\(error)\\(CliSyntaxColor.END)\")\n            }\n        }\n\n        _ = group.wait(timeout: .distantFuture)\n        concurrentStream.waitAndShowCursor()\n\n        print(\"---\")\n    }\n}\n"
  },
  {
    "path": "Sources/PrivacyManifest/main.swift",
    "content": "//\n//  main.swift\n//\n//\n//  Created by Stelios Petrakis on 9/4/24.\n//\n\nimport Foundation\n\nimport ArgumentParser\nimport Spinner\nimport PathKit\n\nstruct PrivacyManifest: ParsableCommand {\n    static let configuration = CommandConfiguration(\n        commandName: \"privacy-manifest\",\n        abstract: \"Privacy Manifest tool\",\n        discussion: \"\"\"\nAn easy and fast way to parse your whole Xcode project, Xcode workspace or\nSwift Package in order to find whether your codebase makes use of Apple's\nrequired reason APIs\n(https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api) or privacy collected data (https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests).\n\n!!! Disclaimer: This tool must *not* be used as the only way to generate the privacy manifest. Do your own research !!!\n\"\"\",\n        version: \"0.0.18\",\n        subcommands: [Analyze.self])\n}\n\nstruct Analyze: ParsableCommand {\n    private static let PRIVACYINFO_FILENAME = \"PrivacyInfo.xcprivacy\"\n\n    // The data structure of the generated PrivacyInfo.xcprivacy file\n    struct PrivacyManifestDataStructure: Encodable {\n        struct PrivacyAccessedAPIType: Encodable {\n            var nsPrivacyAccessedAPIType: String\n            var nSPrivacyAccessedAPITypeReasons: [String]\n\n            enum CodingKeys: String, CodingKey {\n                case nsPrivacyAccessedAPIType = \"NSPrivacyAccessedAPIType\"\n                case nSPrivacyAccessedAPITypeReasons = \"NSPrivacyAccessedAPITypeReasons\"\n            }\n        }\n\n        var nsPrivacyTracking: Bool\n        var nsPrivacyTrackingDomains: [String]\n        var nsPrivacyCollectedDataTypes: [[String:String]]\n        var nsPrivacyAccessedAPITypes: [PrivacyAccessedAPIType]\n\n        enum CodingKeys: String, CodingKey {\n            case nsPrivacyTracking = \"NSPrivacyTracking\"\n            case nsPrivacyTrackingDomains = \"NSPrivacyTrackingDomains\"\n            case nsPrivacyCollectedDataTypes = \"NSPrivacyCollectedDataTypes\"\n            case nsPrivacyAccessedAPITypes = \"NSPrivacyAccessedAPITypes\"\n        }\n    }\n\n    enum DetectedProjectType {\n        case xcodeProject(Path)\n        case xcodeWorkspace(Path)\n        case swiftPackage(Path)\n        case directory(Path)\n    }\n\n    public static let configuration = CommandConfiguration(\n        commandName: \"analyze\",\n        abstract: \"Analyzes the project to detect privacy aware API usage\",\n        discussion: \"\"\"\nSupports Xcode projects (.xcodeproj), Xcode workspaces (.xcworkspace) and\nSwift Packages (Package.swift).\n\n!!! Disclaimer: This tool must *not* be used as the only way to generate the privacy manifest. Do your own research !!!\n\"\"\"\n    )\n    \n    @Option(name: .long, help: \"\"\"\nEither the (relative/absolute) path to the project's\n.xcodeproj(e.g. path/to/MyProject.xcodeproj),\n.xcworkspace (e.g. path/to/MyWorkspace.xcworkspace) or\nPackage.swift (e.g. path/to/Package.swift).\n\"\"\")\n    private var project : String\n\n    @Flag(name: .long, help: \"Reveals the API occurrences on each file.\")\n    var revealOccurrences: Bool = false\n\n    @Flag(name: .long, help: \"\"\"\nLook up the source files of library / framework targets.\n\nBy default, when a library / framework target is encountered, the source files\nare not checked.\n\nWarning: If specified, the process might take a while to complete based on the\namount of project / targets.\n\"\"\")\n    var deepLibraryFrameworkCheck: Bool = false\n\n    @Option(name: .long, help: \"\"\"\nThe path to the directory where the privacy manifest file will be generated (Optional).\n\"\"\")\n    var output: String?\n\n    func run() throws {\n        let projectPath = Path(project).absolute()\n        var detectedProjectType: DetectedProjectType?\n\n        if projectPath.lastComponent == PACKAGE_SWIFT_FILENAME {\n            detectedProjectType = .swiftPackage(projectPath)\n        }\n        else if projectPath.extension == XCODE_PROJECT_PATH_EXTENSION {\n            detectedProjectType = .xcodeProject(projectPath)\n        }\n        else if projectPath.extension == XCODE_WORKSPACE_PATH_EXTENSION {\n            detectedProjectType = .xcodeWorkspace(projectPath)\n        }\n        else if projectPath.isDirectory {\n            // Reverse sort the children paths so that xcworkspace is parsed\n            // first if both .xcodeproj and .xcworkspace are found in the same\n            // path.\n            let children = try projectPath.children().sorted().reversed()\n            guard children.count > 0 else {\n                print(\"\\(CliSyntaxColor.RED)Empty directory: \\(projectPath)\\(CliSyntaxColor.END)\")\n                return\n            }\n            children.forEach { childPath in\n                if detectedProjectType != nil {\n                    return\n                }\n                else if childPath.extension == XCODE_WORKSPACE_PATH_EXTENSION {\n                    detectedProjectType = .xcodeWorkspace(childPath)\n                }\n                else if childPath.extension == XCODE_PROJECT_PATH_EXTENSION {\n                    detectedProjectType = .xcodeProject(childPath)\n                }\n                else if childPath.lastComponent == PACKAGE_SWIFT_FILENAME {\n                    detectedProjectType = .swiftPackage(childPath)\n                }\n            }\n            if detectedProjectType == nil {\n                detectedProjectType = .directory(projectPath)\n            }\n        }\n\n        guard let detectedProjectType = detectedProjectType else {\n            print(\"\\(CliSyntaxColor.RED)File type not supported: \\(projectPath)\\(CliSyntaxColor.END)\")\n            return\n        }\n\n        var requiredAPIs: [RequiredReasonKey: Set<PresentedResult>]?\n\n        do {\n            switch detectedProjectType {\n            case .swiftPackage(let path):\n                print(\"Swift Package detected: \\(CliSyntaxColor.WHITE_BOLD)\\(path)\\(CliSyntaxColor.END)\")\n                let swiftPackage = SwiftPackageProjectParser(with: path)\n                try measure {\n                    try swiftPackage.parse()\n                }\n                requiredAPIs = swiftPackage.process(revealOccurrences: revealOccurrences)\n            case .xcodeProject(let path):\n                print(\"Xcode project detected: \\(CliSyntaxColor.WHITE_BOLD)\\(path)\\(CliSyntaxColor.END)\")\n                let xcodeProject = XcodeProjectParser(with: path,\n                                                      deepLibraryFrameworkCheck: deepLibraryFrameworkCheck)\n                try measure {\n                    try xcodeProject.parse()\n                }\n                requiredAPIs = xcodeProject.process(revealOccurrences: revealOccurrences)\n            case .xcodeWorkspace(let path):\n                print(\"Xcode workspace detected: \\(CliSyntaxColor.WHITE_BOLD)\\(path)\\(CliSyntaxColor.END)\")\n                let xcodeWorkspace = XcodeWorkspaceParser(with: path,\n                                                          deepLibraryFrameworkCheck: deepLibraryFrameworkCheck)\n                try measure {\n                    try xcodeWorkspace.parse()\n                }\n                requiredAPIs = xcodeWorkspace.process(revealOccurrences: revealOccurrences)\n            case .directory(let path):\n                print(\"Directory detected: \\(CliSyntaxColor.WHITE_BOLD)\\(path)\\(CliSyntaxColor.END)\")\n                let xcodeProject = DirectoryProjectParser(with: path)\n                try measure {\n                    try xcodeProject.parse()\n                }\n                requiredAPIs = xcodeProject.process(revealOccurrences: revealOccurrences)\n            }\n        }\n        catch {\n            ConcurrentSpinnerStream.showCursor()\n            print(\"\\n\\(CliSyntaxColor.RED)✖ Parser Error: \\(error)\\(CliSyntaxColor.END)\")\n            return\n        }\n\n        if let output = output,\n           let requiredAPIs = requiredAPIs {\n            print(\"---\")\n\n            let outputPath = Path(output)\n\n            guard outputPath.isDirectory else {\n                print(\"\\(CliSyntaxColor.RED)Error: Output path not a directory\\(CliSyntaxColor.END)\")\n                return\n            }\n            generateManifest(requiredAPIs,\n                             outputPath: Path(output) + Self.PRIVACYINFO_FILENAME)\n        }\n    }\n\n    func measure(function: () throws -> Void) throws {\n        let clock = ContinuousClock()\n        let result = try clock.measure(function)\n        print(\"Execution took \\(result)\")\n    }\n\n    func generateManifest(_ requiredAPIs: [RequiredReasonKey: Set<PresentedResult>],\n                          outputPath: Path) {\n        var manifestReasons: [PrivacyManifestDataStructure.PrivacyAccessedAPIType] = []\n\n        // Show the THIRD_PARTY_SDK_KEY first\n        requiredAPIs.sorted {\n            if $0.key == .THIRD_PARTY_SDK_KEY {\n                return true\n            }\n            else if $1.key == .THIRD_PARTY_SDK_KEY {\n                return false\n            }\n            else {\n                return $0.key < $1.key\n            }\n        }.forEach { (key, value) in\n            guard value.count > 0, key.reasons.count > 0 else {\n                return\n            }\n\n            if key == .THIRD_PARTY_SDK_KEY, let reason = key.reasons.first {\n                var results: Set<String> = Set()\n                value.forEach { result in\n                    results.update(with: result.filePath)\n                }\n                print(\"\\n\\(CliSyntaxColor.WHITE_BOLD)WARNING:\\(CliSyntaxColor.END) The following third-party SDKs were detected:\\n\")\n                print(\"* \\(results.joined(separator: \"\\n* \"))\")\n                print(reason.value)\n                print(\"\\(CliSyntaxColor.CYAN)⚓︎ \\(key.link)\\(CliSyntaxColor.END)\\n\")\n                print(\"ENTER to continue\", terminator: \"\")\n                _ = readLine()\n                return\n            }\n\n            guard let privacyManifestKey = key.privacyManifestKey else {\n                return\n            }\n\n            print(\"\\n\\(CliSyntaxColor.WHITE_BOLD)\\(value.count) \\(value.count == 1 ? \"occurrence\" : \"occurrences\") for \\(key.description)\\(CliSyntaxColor.END). Available reasons:\\n\")\n\n            var index = 0\n            let reasonKeys = [String](key.reasons.keys)\n            reasonKeys.forEach { reasonKey in\n                guard let value = key.reasons[reasonKey] else {\n                    return\n                }\n                print(\"\"\"\n\\(CliSyntaxColor.WHITE_BOLD)\\(index+1).\\(CliSyntaxColor.END) \\(value)\\n\n\"\"\")\n                index += 1\n            }\n\n            print(\"\\(CliSyntaxColor.CYAN)⚓︎ \\(key.link)\\(CliSyntaxColor.END)\\n\")\n\n            print(\"Enter values \\(1)-\\(reasonKeys.count) that match your case (comma separated for multiple values, ENTER for none): \",\n                  terminator: \"\")\n\n            var manifestReasonKeys: [String] = []\n\n            if let input = readLine() {\n                let values = input.components(separatedBy: \",\")\n                values.forEach { value in\n                    guard let index = Int(value.trimmingCharacters(in: .whitespaces)),\n                        index - 1 >= 0 && index - 1 < reasonKeys.count else {\n                        return\n                    }\n                    let reasonKey = reasonKeys[index - 1]\n                    manifestReasonKeys.append(reasonKey)\n                }\n            }\n\n            if manifestReasonKeys.count > 0 {\n                manifestReasons.append(PrivacyManifestDataStructure.PrivacyAccessedAPIType(\n                    nsPrivacyAccessedAPIType: privacyManifestKey,\n                    nSPrivacyAccessedAPITypeReasons: manifestReasonKeys))\n            }\n        }\n\n        print(\"\\n\")\n\n        guard manifestReasons.count > 0 else {\n            print(\"\\(CliSyntaxColor.YELLOW)No reasons were provided, Privacy Manifest file generation was skipped.\\(CliSyntaxColor.END)\")\n            return\n        }\n\n        let privacyManifestDataStructure = PrivacyManifestDataStructure(\n            nsPrivacyTracking: false,\n            nsPrivacyTrackingDomains: [],\n            nsPrivacyCollectedDataTypes: [],\n            nsPrivacyAccessedAPITypes: manifestReasons)\n\n        do {\n            try PropertyListEncoder().encode(privacyManifestDataStructure).write(to: outputPath.url)\n            print(\"\\(CliSyntaxColor.GREEN)✔\\(CliSyntaxColor.END) Privacy Manifest file was generated successfully at \\(CliSyntaxColor.WHITE_BOLD)\\(outputPath.absolute())\\(CliSyntaxColor.END)\")\n            print(\"\"\"\n\\(CliSyntaxColor.YELLOW_BRIGHT)\n⚠--------------------------- Disclaimer ---------------------------+\n| Check the values of the generated Privacy Manifest file before   |\n| submitting your application to the App Store for review, as only |\n| the values of the NSPrivacyAccessedAPITypes dictionary are based |\n| on your responses.                                               |\n+------------------------------------------------------------------+\n\\(CliSyntaxColor.END)\n\"\"\")\n        } catch {\n            print(\"\\(CliSyntaxColor.RED)✖ Error generating Privacy Manifest file: \\(error)\\(CliSyntaxColor.END)\")\n        }\n    }\n}\n\nPrivacyManifest.main()\n"
  }
]