Full Code of stelabouras/privacy-manifest for AI

main 587b03ed28a4 cached
13 files
85.2 KB
19.2k tokens
1 requests
Download .txt
Repository: stelabouras/privacy-manifest
Branch: main
Commit: 587b03ed28a4
Files: 13
Total size: 85.2 KB

Directory structure:
gitextract_3gviv89a/

├── .gitignore
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
└── Sources/
    └── PrivacyManifest/
        ├── Constants.swift
        ├── DirectoryProjectParser.swift
        ├── ProjectParser.swift
        ├── SpinnerStreams.swift
        ├── SwiftPackageProjectParser.swift
        ├── XcodeProjectParser.swift
        └── main.swift

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
.swiftpm/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.



================================================
FILE: Makefile
================================================
install:
		swift build -c release
		install .build/release/privacy-manifest /usr/local/bin/privacy-manifest


================================================
FILE: Package.resolved
================================================
{
  "pins" : [
    {
      "identity" : "aexml",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/tadija/AEXML.git",
      "state" : {
        "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
        "version" : "4.6.1"
      }
    },
    {
      "identity" : "bluesignals",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/IBM-Swift/BlueSignals.git",
      "state" : {
        "revision" : "1f6c49e186c8a4eeef87ba14f2f97b8646559d13",
        "version" : "1.0.200"
      }
    },
    {
      "identity" : "nanoseconds",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/dominicegginton/Nanoseconds",
      "state" : {
        "revision" : "34318d7a13b5b5013102fbe18b36b80368b4dcbd",
        "version" : "1.1.2"
      }
    },
    {
      "identity" : "pathkit",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/kylef/PathKit.git",
      "state" : {
        "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
        "version" : "1.0.1"
      }
    },
    {
      "identity" : "rainbow",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/onevcat/Rainbow",
      "state" : {
        "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3",
        "version" : "4.0.1"
      }
    },
    {
      "identity" : "spectre",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/kylef/Spectre.git",
      "state" : {
        "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
        "version" : "0.10.1"
      }
    },
    {
      "identity" : "spinner",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/dominicegginton/Spinner",
      "state" : {
        "revision" : "16ac0c404320005936579f1d6454bb88dbd8d71a",
        "version" : "2.1.0"
      }
    },
    {
      "identity" : "swift-argument-parser",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-argument-parser",
      "state" : {
        "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
        "version" : "1.2.3"
      }
    },
    {
      "identity" : "swift-asn1",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-asn1.git",
      "state" : {
        "revision" : "c7e239b5c1492ffc3ebd7fbcc7a92548ce4e78f0",
        "version" : "1.1.0"
      }
    },
    {
      "identity" : "swift-certificates",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-certificates.git",
      "state" : {
        "revision" : "01d7664523af5c169f26038f1e5d444ce47ae5ff",
        "version" : "1.0.1"
      }
    },
    {
      "identity" : "swift-collections",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-collections.git",
      "state" : {
        "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192",
        "version" : "1.0.6"
      }
    },
    {
      "identity" : "swift-crypto",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-crypto.git",
      "state" : {
        "revision" : "629f0b679d0fd0a6ae823d7f750b9ab032c00b80",
        "version" : "3.0.0"
      }
    },
    {
      "identity" : "swift-driver",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-driver.git",
      "state" : {
        "branch" : "release/5.10",
        "revision" : "46bd60c4934aa8512061b8182f59dcc5f0a25fd0"
      }
    },
    {
      "identity" : "swift-llbuild",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-llbuild.git",
      "state" : {
        "branch" : "release/5.10",
        "revision" : "fd7c2e0d9279edd023ced6b0a590f8407f5472f9"
      }
    },
    {
      "identity" : "swift-package-manager",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-package-manager",
      "state" : {
        "branch" : "release/5.10",
        "revision" : "a0e7a8aef2989e315d0fe2180a5cbe2b9c8dc150"
      }
    },
    {
      "identity" : "swift-system",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-system.git",
      "state" : {
        "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574",
        "version" : "1.1.1"
      }
    },
    {
      "identity" : "swift-tools-support-core",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-tools-support-core.git",
      "state" : {
        "branch" : "release/5.10",
        "revision" : "3695ee46daf7604bec9e16337a60aa62cd261774"
      }
    },
    {
      "identity" : "xcodeproj",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/tuist/XcodeProj.git",
      "state" : {
        "revision" : "313aaf1ad612135b7b0ccf731c86b5c07bf149b5",
        "version" : "8.20.0"
      }
    },
    {
      "identity" : "yams",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/jpsim/Yams.git",
      "state" : {
        "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
        "version" : "5.0.6"
      }
    }
  ],
  "version" : 2
}


================================================
FILE: Package.swift
================================================
// swift-tools-version:5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "PrivacyManifest",
    platforms: [
        .macOS(.v13)
    ],
    products: [
        .executable(name: "privacy-manifest",
                    targets: ["PrivacyManifest"])
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser",
                 exact: "1.2.3"),
        .package(url: "https://github.com/apple/swift-package-manager",
                 branch: "release/5.10"),
        .package(url: "https://github.com/tuist/XcodeProj",
                 exact: "8.20.0"),
        .package(url: "https://github.com/dominicegginton/Spinner",
                 exact: "2.1.0")
    ],
    targets: [
        .executableTarget(
            name: "PrivacyManifest",
            dependencies: [
                "XcodeProj",
                "Spinner",
                .product(name: "ArgumentParser",
                         package: "swift-argument-parser"),
                .product(name: "SwiftPM-auto", 
                         package: "swift-package-manager"),
            ]
        )
    ]
)


================================================
FILE: README.md
================================================
# Privacy Manifest

<p align="left">
<img src="https://img.shields.io/badge/macOS_v13%2B-_?style=flat&logo=apple&label=platform">
</p>

Privacy Manifest CLI tool parses an Xcode project/workspace or a Swift Package
and attempts to locate calls to Apple's required reason APIs [^1] and detect
privacy collected data frameworks [^2].

The tool detects and parses the source files of the project as well as the
frameworks added in the Xcode project's Build Phase or in the Swift Package
dependencies. It also detects any frameworks / static libraries and checks if
they are included in the third-party SDK list that Apple has provided [^3].

The tool does not perform any sort of analysis beyond the simple
line-by-line check for the existence of the method calls or symbols that
Apple has already published.

The tool uses a concurrent queue to speed up the parsing process.

## Installation

You can either use the tool by typing: `swift run privacy-manifest` in the root
directory of the project, or you can install the executable to `/usr/local/bin`
directory so that you can call it from any folder.

Check out the project and run the following command in the project root 
to install the binary to `/usr/local/bin`.

```sh
sudo make install
```


## Usage

After installing the tool to the `/usr/local/bin` directory, you can invoke it
from any directory using the following command:

```
privacy-manifest analyze --project path/to/project --reveal-occurrences --output path
```

The `path/to/project` can be a relative or an absolute path to the `.xcodeproj`
or `Package.swift` file of your project.

The `--reveal-occurrences` is an optional flag that displays extended information
regarding the occurrences of the required reason APIs / privacy collected data
frameworks in your codebase, highlighting the file and the line where a call has
been detected.

The `--output` flag is optional and if specified, a `PrivacyInfo.xcprivacy`
property list file will be generated to that directory based on the detected
required reason APIs and from the responses of the user.

## Example

Below is the console output from the [VLC iOS OSS](https://github.com/videolan/vlc-ios).

![Privacy Manifest analyze running for VLC iOS project](https://raw.githubusercontent.com/stelabouras/privacy-manifest/main/.github/privacymanifest-vlc.gif)

## Future implementations

The tool can output the occurrences report to HTML for better readability.

On top of that, the list of third-party crash frameworks can be updated so that
it can inform the user when such framework is detected (there is a related TODO
in the code).

## Disclaimer

Do not use this tool alone to create your privacy manifest file for your app or
SDK. You must always double-check the occurrences that the tool displays as the
tool does not know whether a certain occurrence is included in a comment or on
an unused piece of code. Furthermore, there might also be cases where something
has not been included in the parsing process.

This tool gives you a high-level overview of the different required reason APIs
and privacy collected data frameworks your project, workspace or package uses,
so always do your own research after using this tool, to confirm the findings.

## License

Licensed under Apache License 2.0, see [LICENSE](LICENSE) file.

[^1]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
[^2]: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests).
[^3]: https://developer.apple.com/support/third-party-SDK-requirements/


================================================
FILE: Sources/PrivacyManifest/Constants.swift
================================================
//
//  Constants.swift
//
//
//  Created by Stelios Petrakis on 14/4/24.
//

import Foundation

struct CliSyntaxColor {
    static let WHITE_BOLD = "\u{001B}[0;1m"
    static let RED = "\u{001B}[0;0;31m"
    static let GREEN = "\u{001B}[0;32m"
    static let YELLOW = "\u{001B}[0;33m"
    static let BLUE = "\u{001B}[0;34m"
    static let MAGENTA = "\u{001B}[0;35m"
    static let CYAN = "\u{001B}[0;36m"
    static let PINK = "\u{001B}[0;91m"
    static let GREEN_BRIGHT = "\u{001B}[0;92m"
    static let YELLOW_BRIGHT = "\u{001B}[0;93m"
    static let BLUE_BRIGHT = "\u{001B}[0;94m"
    static let MAGENTA_BRIGHT = "\u{001B}[0;95m"
    static let CYAN_BRIGHT = "\u{001B}[0;96m"
    static let END = "\u{001B}[0;0m"
}

let PACKAGE_SWIFT_FILENAME = "Package.swift"
let XCODE_PROJECT_PATH_EXTENSION = "xcodeproj"
let XCODE_WORKSPACE_PATH_EXTENSION = "xcworkspace"

enum RequiredReasonKey: CaseIterable, Comparable {
    case FILE_TIMESTAMP_APIS_KEY
    case SYSTEM_BOOT_APIS_KEY
    case DISK_SPACE_APIS_KEY
    case ACTIVE_KEYBOARD_APIS_KEY
    case USER_DEFAULTS_APIS_KEY
    case CORELOCATION_FRAMEWORK_KEY
    case HEALTHKIT_FRAMEWORK_KEY
    case CRASH_FRAMEWORK_KEY
    case CONTACTS_FRAMEWORK_KEY
    case THIRD_PARTY_SDK_KEY
    
    var description: String {
        switch self {
        case .FILE_TIMESTAMP_APIS_KEY:
            return "File Timestamp APIs"
        case .SYSTEM_BOOT_APIS_KEY:
            return "System boot time APIs"
        case .DISK_SPACE_APIS_KEY:
            return "Disk space APIs"
        case .ACTIVE_KEYBOARD_APIS_KEY:
            return "Active keyboard APIs"
        case .USER_DEFAULTS_APIS_KEY:
            return "User defaults APIs"
        case .CORELOCATION_FRAMEWORK_KEY:
            return "Core Location"
        case .HEALTHKIT_FRAMEWORK_KEY:
            return "HealthKit"
        case .CRASH_FRAMEWORK_KEY:
            return "Crash data"
        case .CONTACTS_FRAMEWORK_KEY:
            return "Contacts"
        case .THIRD_PARTY_SDK_KEY:
            return "Third-party SDKs"
        }
    }

    var privacyManifestKey: String? {
        switch self {
        case .FILE_TIMESTAMP_APIS_KEY:
            return "NSPrivacyAccessedAPICategoryFileTimestamp"
        case .SYSTEM_BOOT_APIS_KEY:
            return "NSPrivacyAccessedAPICategorySystemBootTime"
        case .DISK_SPACE_APIS_KEY:
            return "NSPrivacyAccessedAPICategoryDiskSpace"
        case .ACTIVE_KEYBOARD_APIS_KEY:
            return "NSPrivacyAccessedAPICategoryActiveKeyboards"
        case .USER_DEFAULTS_APIS_KEY:
            return "NSPrivacyAccessedAPICategoryUserDefaults"
        default:
            return nil
        }
    }

    var reasons: [String: String] {
        switch self {
        case .DISK_SPACE_APIS_KEY:
            return [
                "85F4.1" : """
Declare this reason to display disk space information to the
person using the device.
Disk space may be displayed in units of information (such as bytes)
or units of time combined with a media type (such as minutes of HD video).

Information accessed for this reason, or any derived information,
may not be sent off-device. There is an exception that allows the app to
send disk space information over the local network to another device
operated by the same person only for the purpose of displaying disk space
information on that device; this exception only applies if the user has
provided explicit permission to send disk space information,
and the information may not be sent over the Internet.
""",
                "E174.1" : """
Declare this reason to check whether there is sufficient disk space to
write files, or to check whether the disk space is low so that the app can
delete files when the disk space is low. The app must behave differently
based on disk space in a way that is observable to users.

Information accessed for this reason, or any derived information,
may not be sent off-device. There is an exception that allows the app to
avoid downloading files from a server when disk space is insufficient.
""",
                "7D9E.1" : """
Declare this reason to include disk space information in an optional bug
report that the person using the device chooses to submit.
The disk space information must be prominently displayed to the person as
part of the report.

Information accessed for this reason, or any derived information,
may be sent off-device only after the user affirmatively chooses to submit
the specific bug report including disk space information,
and only for the purpose of investigating or responding to the bug report.
""",
                "B728.1" : """
Declare this reason if your app is a health research app, and you access
this API category to detect and inform research participants about low disk
space impacting the research data collection.

Your app must comply with App Store Review Guideline §5.1.3.
Your app must not offer any functionality other than providing information
about and allowing people to participate in health research.
"""
            ]
        case .FILE_TIMESTAMP_APIS_KEY:
            return [
                "DDA9.1" : """
Declare this reason to display file timestamps to the person using the device.

Information accessed for this reason, or any derived information,
may not be sent off-device.
""",
                "C617.1" : """
Declare this reason to access the timestamps, size, or other metadata of files
inside the app container, app group container, or the app’s CloudKit container.
""",
                "3B52.1" : """
Declare this reason to access the timestamps, size, or other metadata of files
or directories that the user specifically granted access to, such as using
a document picker view controller.
""",
                "0A2A.1" : """
Declare this reason if your third-party SDK is providing a wrapper function
around file timestamp API(s) for the app to use, and you only access the
file timestamp APIs when the app calls your wrapper function. This reason may
only be declared by third-party SDKs. This reason may not be declared if your
third-party SDK was created primarily to wrap required reason API(s).

Information accessed for this reason, or any derived information, 
may not be used for your third-party SDK’s own purposes or sent off-device
by your third-party SDK.
"""
            ]
        case .SYSTEM_BOOT_APIS_KEY:
            return [
                "35F9.1" : """
Declare this reason to access the system boot time in order to measure the
amount of time that has elapsed between events that occurred within the app
or to perform calculations to enable timers.

Information accessed for this reason, or any derived information,
may not be sent off-device. There is an exception for information about the
amount of time that has elapsed between events that occurred within the app,
which may be sent off-device.
""",
                "8FFB.1" : """
Declare this reason to access the system boot time to calculate absolute
timestamps for events that occurred within your app, such as events related
to the UIKit or AVFAudio frameworks.

Absolute timestamps for events that occurred within your app may be sent
off-device. System boot time accessed for this reason, or any other information
derived from system boot time, may not be sent off-device.
""",
                "3D61.1" : """
Declare this reason to include system boot time information in an optional
bug report that the person using the device chooses to submit.
The system boot time information must be prominently displayed to the person
as part of the report.

Information accessed for this reason, or any derived information,
may be sent off-device only after the user affirmatively chooses to submit
the specific bug report including system boot time information,
and only for the purpose of investigating or responding to the bug report.
"""
            ]
        case .ACTIVE_KEYBOARD_APIS_KEY:
            return [
                "3EC4.1" : """
Declare this reason if your app is a custom keyboard app, and you access
this API category to determine the keyboards that are active on the device.

Providing a systemwide custom keyboard to the user must be the primary
functionality of the app.

Information accessed for this reason, or any derived information,
may not be sent off-device.
""",
                "54BD.1" : """
Declare this reason to access active keyboard information to present
the correct customized user interface to the person using the device.
The app must have text fields for entering or editing text and must behave
differently based on active keyboards in a way that is observable to users.

Information accessed for this reason, or any derived information,
may not be sent off-device.
"""
            ]
        case .USER_DEFAULTS_APIS_KEY:
            return [
                "CA92.1" : """
Declare this reason to access user defaults to read and write information
that is only accessible to the app itself.

This reason does not permit reading information that was written by
other apps or the system, or writing information that can be accessed by
other apps.
""",
                "1C8F.1" : """
Declare this reason to access user defaults to read and write information
that is only accessible to the apps, app extensions, and App Clips that
are members of the same App Group as the app itself.

This reason does not permit reading information that was written by apps,
app extensions, or App Clips outside the same App Group or by the system.
Your app is not responsible if the system provides information from the
global domain because a key is not present in your requested domain while
your app is attempting to read information that apps, app extensions,
or App Clips in your app’s App Group write.

This reason also does not permit writing information that can be accessed
by apps, app extensions, or App Clips outside the same App Group.
""",
                "C56D.1" : """
Declare this reason if your third-party SDK is providing a wrapper function
around user defaults API(s) for the app to use, and you only access the
user defaults APIs when the app calls your wrapper function.
This reason may only be declared by third-party SDKs.
This reason may not be declared if your third-party SDK was created primarily
to wrap required reason API(s).

Information accessed for this reason, or any derived information,
may not be used for your third-party SDK’s own purposes or sent off-device
by your third-party SDK.
""",
                "AC6B.1" : """
Declare this reason to access user defaults to read the
com.apple.configuration.managed key to retrieve the managed app configuration
set by MDM, or to set the com.apple.feedback.managed key to store
feedback information to be queried over MDM, as described in the
Apple Mobile Device Management Protocol Reference documentation.
"""
            ]
        case .CORELOCATION_FRAMEWORK_KEY:
            return [:] // TODO
        case .HEALTHKIT_FRAMEWORK_KEY:
            return [:] // TODO
        case .CRASH_FRAMEWORK_KEY:
            return [:] // TODO
        case .CONTACTS_FRAMEWORK_KEY:
            return [:] // TODO
        case .THIRD_PARTY_SDK_KEY:
            return [
                "reason" : """
\(CliSyntaxColor.BLUE_BRIGHT)
ℹ---------------------------------------------------------------------------------+
| You must include the privacy manifest for any of the above SDKs when you submit |
| new apps in App Store Connect that include those SDKs, or when you submit an    |
| app update that adds one of the listed SDKs as part of the update.              |
| Signatures are also required in these cases where the listed SDKs are used      |
| as binary dependencies. Any version of a listed SDK, as well as any SDKs that   |
| repackage those on the list, are included in the requirement.                   |
+---------------------------------------------------------------------------------+
\(CliSyntaxColor.END)
"""]
        }
    }

    var link: String {
        switch self {
        case .FILE_TIMESTAMP_APIS_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393"
        case .SYSTEM_BOOT_APIS_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394"
        case .DISK_SPACE_APIS_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397"
        case .ACTIVE_KEYBOARD_APIS_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400"
        case .USER_DEFAULTS_APIS_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401"
        case .CORELOCATION_FRAMEWORK_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263133"
        case .HEALTHKIT_FRAMEWORK_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263132"
        case .CRASH_FRAMEWORK_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263159"
        case .CONTACTS_FRAMEWORK_KEY:
            return "https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4263130"
        case .THIRD_PARTY_SDK_KEY:
            return "https://developer.apple.com/support/third-party-SDK-requirements/"
        }
    }
}

let ALLOWED_EXTENSIONS = [
    "m",    // Objective-C
    "mm",   // Objective-C++
    "c",    // C
    "cpp",  // C++
    "swift" // Swift
]

// Look through the code for the listed strings (Case Sensitive)
let APIS_TO_CHECK: [String: [RequiredReasonKey]] = [
    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
    "creationDate": [.FILE_TIMESTAMP_APIS_KEY],
    "modificationDate": [.FILE_TIMESTAMP_APIS_KEY],
    "fileModificationDate": [.FILE_TIMESTAMP_APIS_KEY],
    "contentModificationDateKey": [.FILE_TIMESTAMP_APIS_KEY],
    "creationDateKey": [.FILE_TIMESTAMP_APIS_KEY],
    "getattrlist(": [.FILE_TIMESTAMP_APIS_KEY, .DISK_SPACE_APIS_KEY], // also covers: fgetattrlist(
    "getattrlistbulk(": [.FILE_TIMESTAMP_APIS_KEY],
    "fstat(": [.FILE_TIMESTAMP_APIS_KEY],
    "fstatat(": [.FILE_TIMESTAMP_APIS_KEY],
    "lstat(": [.FILE_TIMESTAMP_APIS_KEY],
    "getattrlistat(": [.FILE_TIMESTAMP_APIS_KEY, .DISK_SPACE_APIS_KEY],
    "systemUptime": [.SYSTEM_BOOT_APIS_KEY],
    "mach_absolute_time(": [.SYSTEM_BOOT_APIS_KEY],

    "volumeAvailableCapacityKey": [.DISK_SPACE_APIS_KEY],
    "volumeAvailableCapacityForImportantUsageKey": [.DISK_SPACE_APIS_KEY],
    "volumeAvailableCapacityForOpportunisticUsageKey": [.DISK_SPACE_APIS_KEY],
    "volumeTotalCapacityKey": [.DISK_SPACE_APIS_KEY],
    "systemFreeSize": [.DISK_SPACE_APIS_KEY],
    "systemSize": [.DISK_SPACE_APIS_KEY],
    "statfs(": [.DISK_SPACE_APIS_KEY], // also covers: fstatfs(
    "statvfs(": [.DISK_SPACE_APIS_KEY], // also covers: fstatvfs(

    "activeInputModes": [.ACTIVE_KEYBOARD_APIS_KEY],

    "UserDefaults": [.USER_DEFAULTS_APIS_KEY],

    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests
    "import CoreLocation": [.CORELOCATION_FRAMEWORK_KEY],
    "#import <CoreLocation/CoreLocation.h>": [.CORELOCATION_FRAMEWORK_KEY],

    "import HealthKit": [.HEALTHKIT_FRAMEWORK_KEY],
    "#import <HealthKit/HealthKit.h>": [.HEALTHKIT_FRAMEWORK_KEY],
    "#import <HealthKitUI/HealthKitUI.h>": [.HEALTHKIT_FRAMEWORK_KEY],

    "import Sentry": [.CRASH_FRAMEWORK_KEY],
    "#import <Sentry/Sentry.h>": [.CRASH_FRAMEWORK_KEY],
    "import Instabug": [.CRASH_FRAMEWORK_KEY],
    "#import <Instabug/Instabug.h>": [.CRASH_FRAMEWORK_KEY],
    "#import \"Countly.h\"": [.CRASH_FRAMEWORK_KEY],
    "import Bugsnag": [.CRASH_FRAMEWORK_KEY],
    "#import <Bugsnag/Bugsnag.h>": [.CRASH_FRAMEWORK_KEY],
    "#import <Firebase": [.CRASH_FRAMEWORK_KEY],
    "import Firebase": [.CRASH_FRAMEWORK_KEY],
    // TODO: Add more third-party crash frameworks here

    "import Contacts": [.CONTACTS_FRAMEWORK_KEY],
    "#import <ContactsUI/ContactsUI.h>": [.CONTACTS_FRAMEWORK_KEY],
    "#import <Contacts/Contacts.h>": [.CONTACTS_FRAMEWORK_KEY]
]

// Look through the Frameworks Build Phase or Package Dependencies for the
// listed strings (Case Insensitive)
let SDKS_TO_CHECK: [String: RequiredReasonKey] = [
    // Ref: https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests
    "sentry-cocoa": .CRASH_FRAMEWORK_KEY,
    "Sentry": .CRASH_FRAMEWORK_KEY,
    "Instabug": .CRASH_FRAMEWORK_KEY,
    "countly": .CRASH_FRAMEWORK_KEY,
    "Bugsnag": .CRASH_FRAMEWORK_KEY,
    "firebase": .CRASH_FRAMEWORK_KEY,
    // TODO: Add more third-party crash frameworks here

    "CoreLocation": .CORELOCATION_FRAMEWORK_KEY,
    "HealthKit": .HEALTHKIT_FRAMEWORK_KEY,
    "Contacts": .CONTACTS_FRAMEWORK_KEY, // also covers ContactsUI

    // Ref: https://developer.apple.com/support/third-party-SDK-requirements/
    "Abseil": .THIRD_PARTY_SDK_KEY,
    "AFNetworking": .THIRD_PARTY_SDK_KEY,
    "Alamofire": .THIRD_PARTY_SDK_KEY,
    "AppAuth": .THIRD_PARTY_SDK_KEY, // also covers: GTMAppAuth
    "BoringSSL": .THIRD_PARTY_SDK_KEY,
    "openssl_grpc": .THIRD_PARTY_SDK_KEY,
    "Capacitor": .THIRD_PARTY_SDK_KEY,
    "Charts": .THIRD_PARTY_SDK_KEY,
    "connectivity_plus": .THIRD_PARTY_SDK_KEY,
    "Cordova": .THIRD_PARTY_SDK_KEY,
    "device_info_plus": .THIRD_PARTY_SDK_KEY,
    "DKImagePickerController": .THIRD_PARTY_SDK_KEY,
    "DKPhotoGallery": .THIRD_PARTY_SDK_KEY,
    "FBAEMKit": .THIRD_PARTY_SDK_KEY,
    "FBLPromises": .THIRD_PARTY_SDK_KEY,
    "FBSDKCoreKit": .THIRD_PARTY_SDK_KEY, // also covers: FBSDKCoreKit_Basics
    "FBSDKLoginKit": .THIRD_PARTY_SDK_KEY,
    "FBSDKShareKit": .THIRD_PARTY_SDK_KEY,
    "file_picker": .THIRD_PARTY_SDK_KEY,
    "FirebaseABTesting": .THIRD_PARTY_SDK_KEY,
    "FirebaseAuth": .THIRD_PARTY_SDK_KEY,
    "FirebaseCore": .THIRD_PARTY_SDK_KEY, // also covers: FirebaseCoreDiagnostics, FirebaseCoreExtension, FirebaseCoreInternal
    "FirebaseCrashlytics": .THIRD_PARTY_SDK_KEY,
    "FirebaseDynamicLinks": .THIRD_PARTY_SDK_KEY,
    "FirebaseFirestore": .THIRD_PARTY_SDK_KEY,
    "FirebaseInstallations": .THIRD_PARTY_SDK_KEY,
    "FirebaseMessaging": .THIRD_PARTY_SDK_KEY,
    "FirebaseRemoteConfig": .THIRD_PARTY_SDK_KEY,
    "Flutter": .THIRD_PARTY_SDK_KEY, // also covers: flutter_inappwebview, flutter_local_notifications, fluttertoast
    "FMDB": .THIRD_PARTY_SDK_KEY,
    "geolocator_apple": .THIRD_PARTY_SDK_KEY,
    "GoogleDataTransport": .THIRD_PARTY_SDK_KEY,
    "GoogleSignIn": .THIRD_PARTY_SDK_KEY,
    "GoogleToolboxForMac": .THIRD_PARTY_SDK_KEY,
    "GoogleUtilities": .THIRD_PARTY_SDK_KEY,
    "grpcpp": .THIRD_PARTY_SDK_KEY,
    "GTMSessionFetcher": .THIRD_PARTY_SDK_KEY,
    "hermes": .THIRD_PARTY_SDK_KEY,
    "image_picker_ios": .THIRD_PARTY_SDK_KEY,
    "IQKeyboardManager": .THIRD_PARTY_SDK_KEY, // also covers: IQKeyboardManagerSwift
    "Kingfisher": .THIRD_PARTY_SDK_KEY,
    "leveldb": .THIRD_PARTY_SDK_KEY,
    "Lottie": .THIRD_PARTY_SDK_KEY,
    "MBProgressHUD": .THIRD_PARTY_SDK_KEY,
    "nanopb": .THIRD_PARTY_SDK_KEY,
    "OneSignal": .THIRD_PARTY_SDK_KEY, // also covers: OneSignalCore, OneSignalExtension, OneSignalOutcomes
    "OpenSSL": .THIRD_PARTY_SDK_KEY,
    "OrderedSet": .THIRD_PARTY_SDK_KEY,
    "package_info": .THIRD_PARTY_SDK_KEY, // also covers: package_info_plus
    "path_provider": .THIRD_PARTY_SDK_KEY, // also covers: path_provider_ios
    "Promises": .THIRD_PARTY_SDK_KEY,
    "Protobuf": .THIRD_PARTY_SDK_KEY,
    "Reachability": .THIRD_PARTY_SDK_KEY,
    "RealmSwift": .THIRD_PARTY_SDK_KEY,
    "RxCocoa": .THIRD_PARTY_SDK_KEY,
    "RxRelay": .THIRD_PARTY_SDK_KEY,
    "RxSwift": .THIRD_PARTY_SDK_KEY,
    "SDWebImage": .THIRD_PARTY_SDK_KEY,
    "share_plus": .THIRD_PARTY_SDK_KEY,
    "shared_preferences_ios": .THIRD_PARTY_SDK_KEY,
    "SnapKit": .THIRD_PARTY_SDK_KEY,
    "sqflite": .THIRD_PARTY_SDK_KEY,
    "Starscream": .THIRD_PARTY_SDK_KEY,
    "SVProgressHUD": .THIRD_PARTY_SDK_KEY,
    "SwiftyGif": .THIRD_PARTY_SDK_KEY,
    "SwiftyJSON": .THIRD_PARTY_SDK_KEY,
    "Toast": .THIRD_PARTY_SDK_KEY,
    "UnityFramework": .THIRD_PARTY_SDK_KEY,
    "url_launcher": .THIRD_PARTY_SDK_KEY, // also covers: url_launcher_ios
    "video_player_avfoundation": .THIRD_PARTY_SDK_KEY,
    "wakelock": .THIRD_PARTY_SDK_KEY,
    "webview_flutter_wkwebview": .THIRD_PARTY_SDK_KEY
]


================================================
FILE: Sources/PrivacyManifest/DirectoryProjectParser.swift
================================================
//
//  DirectoryProjectParser.swift
//  
//
//  Created by Stelios Petrakis on 15/4/24.
//

import Foundation

import PathKit

// Recursively rarses all the children files of the provided path, detects the
// supported files and parses them.
class DirectoryProjectParser: ProjectParser {
    override func parse() throws {
        print("---")

        let targetName = projectPath.lastComponent
        let spinner = concurrentStream.createSilentSpinner(with: "Parsing \(CliSyntaxColor.GREEN)\(targetName)'s\(CliSyntaxColor.END) source files...")
        concurrentStream.start(spinner: spinner)
        do {
            var filePathsForParsing: [Path] = []
            try self.projectPath.recursiveChildren().forEach { path in
                guard let ext = path.extension,
                      ALLOWED_EXTENSIONS.contains(ext) else {
                    return
                }
                filePathsForParsing.append(path)
            }
            try self.parseFiles(filePathsForParsing: filePathsForParsing,
                                targetName: targetName,
                                spinner: spinner)
            self.concurrentStream.success(spinner: spinner,
                                          "Parsed \(CliSyntaxColor.GREEN)\(targetName)'s\(CliSyntaxColor.END) source files")
        }
        catch {
            self.concurrentStream.error(spinner: spinner,
                                        "Error parsing \(CliSyntaxColor.GREEN)\(targetName)'s\(CliSyntaxColor.END) source files: \(CliSyntaxColor.RED)\(error)\(CliSyntaxColor.END)")
        }

        _ = group.wait(timeout: .distantFuture)
        concurrentStream.waitAndShowCursor()
    }
}


================================================
FILE: Sources/PrivacyManifest/ProjectParser.swift
================================================
//
//  ProjectParser.swift
//
//
//  Created by Stelios Petrakis on 14/4/24.
//

import Foundation

import Spinner
import PathKit

struct ParsedResult: Hashable {
    var line: String
    var lineNumber: Int?
    var range: Range<String.Index>
}

struct PresentedResult: Hashable {
    var filePath: String
    var formattedLine: String?
    var parsedResult: ParsedResult?
}

class ProjectParser {
    private var requiredAPIs: [RequiredReasonKey: Set<PresentedResult>] = [:]
    private let requiredAPIsLock = NSLock()

    let concurrentStream = ConcurrentSpinnerStream()
    let group = DispatchGroup()
    let queue = DispatchQueue(label: "parser",
                              attributes: .concurrent)

    let projectPath: Path

    init(with projectPath: Path) {
        self.projectPath = projectPath
        RequiredReasonKey.allCases.forEach { key in
            requiredAPIs[key] = Set()
        }
    }

    func parse() throws { }

    final func parseFiles(filePathsForParsing: [Path],
                          targetName: String,
                          spinner: Spinner) throws {
        guard filePathsForParsing.count > 0 else {
            return
        }
        var parsed = 1
        for filePath in filePathsForParsing {
            self.queue.async(group: group,
                             execute: DispatchWorkItem(block: {
                defer {
                    parsed += 1
                    self.concurrentStream.message(spinner: spinner,
                                             "Parsing \(CliSyntaxColor.GREEN)\(targetName)'s\(CliSyntaxColor.END) source files (\(parsed)/\(filePathsForParsing.count))...")
                }
                guard let fileHandle = FileHandle(forReadingAtPath: filePath.string) else {
                    return
                }
                do {
                    guard let data = try fileHandle.readToEnd(),
                          let contents = String(data: data, encoding: .utf8) else {
                        return
                    }

                    Self.lookForAPI(contents: contents).forEach { (key, parsedResult) in
                        let highlightedCode = "\(Self.addBracketsToString(parsedResult.line,around: parsedResult.range))"
                        var formattedLine = ""
                        if let lineNumber = parsedResult.lineNumber {
                            formattedLine = "\(CliSyntaxColor.GREEN)\(lineNumber):\(CliSyntaxColor.END)\t\(highlightedCode)"
                        }
                        else {
                            formattedLine = "\(highlightedCode)"
                        }
                        self.updateRequiredAPIs(key,
                                                with: PresentedResult(filePath: filePath.string,
                                                                      formattedLine: formattedLine,
                                                                      parsedResult: parsedResult))
                    }
                }
                catch {
                    self.concurrentStream.error(spinner: spinner, "Error: \(error)")
                }
            }))
        }
    }

    final func updateRequiredAPIs(_ key: RequiredReasonKey,
                                  with presentedResult: PresentedResult) {
        self.requiredAPIsLock.lock()
        self.requiredAPIs[key]?.update(with: presentedResult)
        self.requiredAPIsLock.unlock()
    }

    final func process(revealOccurrences: Bool) -> [RequiredReasonKey: Set<PresentedResult>] {
        print("---")
        requiredAPIs.sorted(by: {
            if $0.value.count == $1.value.count {
                $0.key.hashValue < $1.key.hashValue
            }
            else {
                $0.value.count < $1.value.count
            }
        }).forEach { (key, list) in
            print("\(CliSyntaxColor.WHITE_BOLD)\(key.description) (\(list.count) \(list.count == 1 ? "occurrence" : "occurrences")\(CliSyntaxColor.END))")

            if !revealOccurrences {
                return
            }
            var currentPath = ""
            list.sorted(by: {
                if $0.parsedResult == nil && $1.parsedResult != nil {
                    return true
                } else if $0.parsedResult != nil && $1.parsedResult == nil {
                    return false
                }
                else if let firstParsedResult = $0.parsedResult,
                         let secondParentResult = $1.parsedResult {
                    if $0.filePath == $1.filePath {
                        return firstParsedResult.lineNumber ?? -1 < secondParentResult.lineNumber ?? -1
                    }
                    else {
                        return $0.filePath < $1.filePath
                    }
                }
                return $0.filePath < $1.filePath
            }).forEach { presentedResult in
                if presentedResult.filePath != currentPath {
                    print("\n\t\(presentedResult.formattedLine != nil ? "✎" : "⛺︎") \(presentedResult.filePath)\(presentedResult.formattedLine != nil ? ":" : "")")
                }

                if let formattedLine = presentedResult.formattedLine {
                    print("\t\t\(formattedLine)")
                }

                currentPath = presentedResult.filePath
            }

            print("\n")
        }

        return requiredAPIs
    }
}

// Helper methods
extension ProjectParser {
    static func lookForAPI(contents: String) -> [(RequiredReasonKey, ParsedResult)] {
        var foundAPIs: [(RequiredReasonKey, ParsedResult)] = []
        var lineNumber = 1
        contents.components(separatedBy: .newlines).forEach { line in
            APIS_TO_CHECK.forEach { (api, requiredReasonKeys) in
                let results = mark(searchString: api,
                                   in: line,
                                   lineNumber: lineNumber,
                                   requiredReasonKeys: requiredReasonKeys)
                foundAPIs.append(contentsOf: results)
            }
            lineNumber += 1
        }
        return foundAPIs
    }

    static func mark(searchString: String,
                     in line: String,
                     lineNumber: Int?,
                     caseInsensitive: Bool = false,
                     requiredReasonKeys: [RequiredReasonKey]) -> [(RequiredReasonKey, ParsedResult)] {
        var parsedResults: [(RequiredReasonKey, ParsedResult)] = []
        var searchRange = line.startIndex..<line.endIndex
        while let range = line.range(of: searchString,
                                     options: caseInsensitive ? .caseInsensitive : [],
                                     range: searchRange) {
            requiredReasonKeys.forEach { requiredReasonKey in
                parsedResults.append((requiredReasonKey, ParsedResult(line: line,
                                                                      lineNumber: lineNumber,
                                                                      range: range)))
            }
            searchRange = range.upperBound..<line.endIndex
        }
        return parsedResults
    }

    static func addTagsToString(_ string: String,
                                around range: Range<String.Index>,
                                openingTag: String,
                                closingTag: String) -> String {
        let lowerBoundIndex = range.lowerBound
        let upperBoundIndex = range.upperBound
        var modifiedString = string
        modifiedString.replaceSubrange(lowerBoundIndex..<lowerBoundIndex,
                                       with: openingTag)
        let adjustedUpperBoundIndex = modifiedString.index(upperBoundIndex,
                                                           offsetBy: openingTag.count)
        modifiedString.replaceSubrange(adjustedUpperBoundIndex..<adjustedUpperBoundIndex,
                                       with: closingTag)
        return modifiedString
    }

    static func addBracketsToString(_ string: String,
                                    around range: Range<String.Index>) -> String {
        return addTagsToString(string, around: range,
                               openingTag: CliSyntaxColor.YELLOW,
                               closingTag: CliSyntaxColor.END)
    }
}


================================================
FILE: Sources/PrivacyManifest/SpinnerStreams.swift
================================================
//
//  SpinnerStreams.swift
//
//
//  Created by Stelios Petrakis on 14/4/24.
//

import Foundation

import Spinner

// Displays several different spinner outputs at once
class ConcurrentSpinnerStream {
    // The array of concurrent silent spinner streams to manage
    var silentSpinnerStreams: [SilentSpinnerStream] = []

    private static let MAX_OUTPUT_LINES = 10

    private var previousRows = 0

    // Serial queue that ensures that console output is serial.
    private let queue = DispatchQueue(label: "stream.queue")
    private let group = DispatchGroup()

    // Serial queue that ensures that there are no race conditions on spinner
    // calls.
    private let spinnersQueue = DispatchQueue(label: "spinner.queue")
    private let spinnersGroup = DispatchGroup()

    init() {
        Self.hideCursor()
    }

    // Renders the added silent spinner streams
    // NOTE: Only call it from within the serial qeueue
    private func render() {
        guard silentSpinnerStreams.count > 0 else {
            return
        }

        // Move cursor at the beginning of the previously rendered string
        if previousRows > 0 {
            print("\u{001B}[\(previousRows)F", terminator: "")
        }
        // Clear from cursor to end of screen
        print("\u{001B}[0J", terminator: "")

        // Generate the buffer
        var buffer = ""
        var linesRendered = 0

        silentSpinnerStreams.sorted().forEach { silentSpinner in
            if linesRendered > Self.MAX_OUTPUT_LINES {
                return
            }
            buffer.append(silentSpinner.buffer + "\n")
            linesRendered += 1
        }

        print("\(buffer)", terminator: "")
        fflush(stdout)

        previousRows = linesRendered
    }

    // Hides the cursor from console
    static func hideCursor() {
        print("\u{001B}[?25l", terminator: "")
        fflush(stdout)
    }

    // Shows the cursor to console
    static func showCursor() {
        print("\u{001B}[?25h", terminator: "")
        fflush(stdout)
    }

    func waitAndShowCursor() {
        // Wait until all async requests have been printed
        _ = spinnersGroup.wait(timeout: .distantFuture)
        _ = group.wait(timeout: .distantFuture)

        Self.showCursor()

        silentSpinnerStreams.removeAll()
        previousRows = 0
    }

    // Adds a silent spinner stream
    func add(stream: SilentSpinnerStream) {
        queue.async(group: group,
                    execute: DispatchWorkItem(block: {
            self.silentSpinnerStreams.append(stream)
        }))
    }

    /// Execute an asynchronous task on the serial queue and optionally render the added silent spinner
    /// streams.
    ///
    /// - Parameters:
    ///   - work: The task to be completed asynchronously within the serial queue
    private func executeSpinnerAsync(work: @escaping () -> Void) {
        spinnersQueue.async(group: spinnersGroup,
                            execute: DispatchWorkItem(block: {
            work()
        }))
    }

    fileprivate func executeSpinnerStreamAsync(work: @escaping () -> Void) {
        queue.async(group: group,
                    execute: DispatchWorkItem(block: {
            work()
            self.render()
        }))
    }

    func start(spinner: Spinner) {
        executeSpinnerAsync {
            spinner.start()
        }
    }

    func success(spinner: Spinner, _ message: String) {
        executeSpinnerAsync {
            spinner.success(message)
        }
    }

    func message(spinner: Spinner, _ message: String) {
        executeSpinnerAsync {
            spinner.message(message)
        }
    }

    func error(spinner: Spinner, _ message: String) {
        executeSpinnerAsync {
            spinner.error(message)
        }
    }

    func createSilentSpinner(with message: String) -> Spinner {
        let silentSpinnerStream = SilentSpinnerStream(concurrentStream: self)
        return Spinner(.dots8Bit, message,
                       stream: silentSpinnerStream)
    }
}

// Writes the spinner stream to a buffer, instead of the stdout
class SilentSpinnerStream: SpinnerStream, Comparable {
    static func == (lhs: SilentSpinnerStream, rhs: SilentSpinnerStream) -> Bool {
        lhs.lastUpdated == rhs.lastUpdated
    }
    
    static func < (lhs: SilentSpinnerStream, rhs: SilentSpinnerStream) -> Bool {
        lhs.lastUpdated > rhs.lastUpdated
    }
    
    var buffer = ""
    var lastUpdated: TimeInterval

    private var concurrentStream: ConcurrentSpinnerStream

    init(concurrentStream: ConcurrentSpinnerStream) {
        self.lastUpdated = Date().timeIntervalSince1970
        self.concurrentStream = concurrentStream
        concurrentStream.add(stream: self)
    }

    func write(string: String, terminator: String) {
        guard string.count > 0 else {
            return
        }
        concurrentStream.executeSpinnerStreamAsync(work: {
            self.lastUpdated = Date().timeIntervalSince1970
            // If the message contains a success or an error character, treat
            // it as the final message for that spinner stream.
            guard !self.buffer.contains("✔") && !self.buffer.contains("✖") else {
                return
            }
            self.buffer = string
        })
    }

    func hideCursor() { }

    func showCursor() { }
}


================================================
FILE: Sources/PrivacyManifest/SwiftPackageProjectParser.swift
================================================
//
//  SwiftPackageProjectParser.swift
//  
//
//  Created by Stelios Petrakis on 14/4/24.
//

import Foundation

import Spinner
import PathKit

import PackageModel
import PackageLoading
import PackageGraph
import Workspace
import Basics
import class TSCBasic.Process
import func TSCBasic.tsc_await

extension SwiftSDK {
    package static var `default`: Self {
        get throws {
            // ref: https://github.com/compnerd/swift-package-manager/blob/master/Examples/package-info/Sources/package-info/main.swift#L10
            let swiftCompiler: AbsolutePath? = {
                let string: String
                #if os(macOS)
                string = try! Process.checkNonZeroExit(args: "xcrun", "--sdk", "macosx", "-f", "swiftc").spm_chomp()
                #else
                string = try! Process.checkNonZeroExit(args: "which", "swiftc").spm_chomp()
                #endif
                return try! AbsolutePath(validating: string)
            }()
            return try! SwiftSDK.hostSwiftSDK(swiftCompiler)
        }
    }
}

extension UserToolchain {
    package static var `default`: Self {
        get throws {
            return try .init(swiftSDK: SwiftSDK.default)
        }
    }
}

// Parses all the Swift Package's supported source files and dependencies.
class SwiftPackageProjectParser : ProjectParser {
    override func parse() throws {
        print("---")

        let spinner = Spinner(.dots8Bit,
                              "Resolving graph...")
        spinner.start()
        let manifestLoader = try ManifestLoader(toolchain: UserToolchain.default)
        let packageAbsolutePath = try AbsolutePath(validating: projectPath.string)
        let root = packageAbsolutePath.parentDirectory
        // ref: https://github.com/unsignedapps/swift-create-xcframework/blob/0be3a68c84987493a7d7298027274a0862bc5ccd/Sources/CreateXCFramework/PackageInfo.swift#L93
        let workspace = try Workspace(
            forRootPackage: root,
            customManifestLoader: manifestLoader
        )
        // Only print warning and error messages
        let observability = Basics.ObservabilitySystem { _, diagnostics in
            guard diagnostics.severity != .debug && diagnostics.severity != .info else {
                return
            }
            print("\(diagnostics.severity): \(diagnostics.message)")
        }
        let scope = observability.topScope
        let graph = try workspace.loadPackageGraph(
            rootPath: root,
            observabilityScope: scope
        )
        spinner.success("Resolved graph")

        graph.requiredDependencies.forEach { dependency in
            guard !dependency.kind.isRoot else {
                return
            }

            let dependencyString = dependency.canonicalLocation.description
            let spinner = concurrentStream.createSilentSpinner(with: "Parsing \(CliSyntaxColor.GREEN)\(dependencyString)\(CliSyntaxColor.END) dependency...")
            concurrentStream.start(spinner: spinner)
            queue.async(group: group,
                        execute: DispatchWorkItem(block: {
                SDKS_TO_CHECK.forEach { (key, value) in
                    let markedResults = Self.mark(searchString: key,
                                                  in: dependencyString,
                                                  lineNumber: nil,
                                                  caseInsensitive: true,
                                                  requiredReasonKeys: [value])
                    guard let firstResult = markedResults.first?.1 else {
                        return
                    }
                    let highlightedCode = "\(Self.addBracketsToString(firstResult.line,around: firstResult.range))"
                    let foundInBuildPhase = "Found \(highlightedCode) in dependencies."
                    self.updateRequiredAPIs(value,
                                            with: PresentedResult(filePath: foundInBuildPhase))
                }
                self.concurrentStream.success(spinner: spinner,
                                              "Parsed \(CliSyntaxColor.GREEN)\(dependencyString)\(CliSyntaxColor.END) dependency")
            }))
        }

        // We only care about the targets of the root packages, not the
        // dependencies
        graph.rootPackages.forEach { package in
            package.targets.forEach { target in
                // Exclude test targets
                guard target.type != .test else {
                    return
                }

                let spinner = concurrentStream.createSilentSpinner(with: "Parsing \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files...")
                concurrentStream.start(spinner: spinner)
                var filePathsForParsing: [Path] = []
                let rootDirectory = target.sources.root
                target.sources.relativePaths.forEach { relativePath in
                    guard let ext = relativePath.extension,
                          ALLOWED_EXTENSIONS.contains(ext) else {
                        return
                    }
                    filePathsForParsing.append(Path(rootDirectory.pathString) + relativePath.pathString)
                }
                do {
                    try self.parseFiles(filePathsForParsing: filePathsForParsing,
                                        targetName: target.name,
                                        spinner: spinner)
                    self.concurrentStream.success(spinner: spinner,
                                                  "Parsed \(filePathsForParsing.count) \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files")
                }
                catch {
                    self.concurrentStream.error(spinner: spinner,
                                                "Error parsing \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files: \(CliSyntaxColor.RED)\(error)\(CliSyntaxColor.END)")
                }
            }
        }

        _ = group.wait(timeout: .distantFuture)
        concurrentStream.waitAndShowCursor()

        print("---")
    }
}


================================================
FILE: Sources/PrivacyManifest/XcodeProjectParser.swift
================================================
//
//  XcodeProjectParser.swift
//  
//
//  Created by Stelios Petrakis on 14/4/24.
//

import Foundation

import Spinner
import PathKit

import XcodeProj

// Parses all Xcode projects contained in a Xcode workspace
class XcodeWorkspaceParser: XcodeProjectParser {
    override func parse() throws {
        print("---")

        let xcworkspace = try XCWorkspace(path: projectPath)

        try xcworkspace.data.children.filter {
            Path($0.location.path).extension == XCODE_PROJECT_PATH_EXTENSION
        }.forEach { element in
            print("\(CliSyntaxColor.WHITE_BOLD)\(element.location.path)\(CliSyntaxColor.END)")

            ConcurrentSpinnerStream.hideCursor()

            let projectPath = projectPath.parent() + Path(element.location.path)

            try parseProject(projectPath)
        }
    }
}

// Parses all targets' supported source files and frameworks.
class XcodeProjectParser: ProjectParser {
    var deepLibraryFrameworkCheck = false

    init(with path: Path,
         deepLibraryFrameworkCheck: Bool = false) {
        super.init(with: path)
        self.deepLibraryFrameworkCheck = deepLibraryFrameworkCheck
    }

    override func parse() throws {
        print("---")

        try parseProject(projectPath)
    }

    fileprivate func parseProject(_ path: Path) throws {
        let xcodeproj = try XcodeProj(path: path)

        // Gather all local and remote package dependencies from the root
        // project.
        var packageDepedencyNames: [String] = []
        do {
            try xcodeproj.pbxproj.rootProject()?.localPackages.compactMap {
                $0.name
            }.forEach {
                packageDepedencyNames.append($0)
            }
            try xcodeproj.pbxproj.rootProject()?.remotePackages.compactMap {
                $0.name
            }.forEach {
                packageDepedencyNames.append($0)
            }
        }
        catch {
            // Suppress any errors due to missing references.
        }

        packageDepedencyNames.forEach { packageDepedencyName in
            let spinner = concurrentStream.createSilentSpinner(with: "Looking up \(CliSyntaxColor.GREEN)\(packageDepedencyName)'s\(CliSyntaxColor.END) package dependency...")
            concurrentStream.start(spinner: spinner)
            queue.async(group: group,
                        execute: DispatchWorkItem(block: {
                SDKS_TO_CHECK.forEach { (key, value) in
                    let markedResults = Self.mark(searchString: key,
                                                  in: packageDepedencyName,
                                                  lineNumber: nil,
                                                  caseInsensitive: true,
                                                  requiredReasonKeys: [value])
                    guard let firstResult = markedResults.first?.1 else {
                        return
                    }
                    let highlightedCode = "\(Self.addBracketsToString(firstResult.line,around: firstResult.range))"
                    let foundInBuildPhase = "Found \(highlightedCode) in Package Dependencies."
                    self.updateRequiredAPIs(value,
                                            with: PresentedResult(filePath: foundInBuildPhase))
                }
                self.concurrentStream.success(spinner: spinner,
                                              "Parsed \(CliSyntaxColor.GREEN)\(packageDepedencyName)\(CliSyntaxColor.END) package dependency")
            }))
        }

        try xcodeproj.pbxproj.nativeTargets.forEach { target in
            guard let productType = target.productType else {
                return
            }

            // Skip UI / Unit tests
            if productType == .unitTestBundle || productType == .uiTestBundle {
                return
            }

            if productType == .staticLibrary
                || productType == .staticFramework
                || productType == .framework
                || productType == .xcFramework {
                let spinner = concurrentStream.createSilentSpinner(with: "Looking up \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) name...")
                concurrentStream.start(spinner: spinner)
                queue.async(group: group,
                            execute: DispatchWorkItem(block: {
                    SDKS_TO_CHECK.forEach { (key, value) in
                        let markedResults = Self.mark(searchString: key,
                                                      in: target.name,
                                                      lineNumber: nil,
                                                      caseInsensitive: true,
                                                      requiredReasonKeys: [value])
                        guard let firstResult = markedResults.first?.1 else {
                            return
                        }
                        let highlightedCode = "\(Self.addBracketsToString(firstResult.line,around: firstResult.range))"
                        let foundInBuildPhase = "Found \(highlightedCode)."
                        self.updateRequiredAPIs(value,
                                                with: PresentedResult(filePath: foundInBuildPhase))
                    }
                    self.concurrentStream.success(spinner: spinner,
                                                  "Looked up \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) name")
                }))
                // Do not proceed into looking at the build phase or the source
                // files of the project targets that are libraries or frameworks
                // if not instructed.
                if !deepLibraryFrameworkCheck {
                    return
                }
            }

            // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests
            target.buildPhases.forEach { phase in
                guard phase.buildPhase == .frameworks else {
                    return
                }
                guard let files = phase.files, files.count > 0 else {
                    return
                }
                let spinner = concurrentStream.createSilentSpinner(with: "Parsing \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) Frameworks Build Phase...")
                concurrentStream.start(spinner: spinner)
                queue.async(group: group,
                            execute: DispatchWorkItem(block: {
                    files.forEach({ file in
                        guard let fullFileName = file.file?.name else {
                            return
                        }
                        SDKS_TO_CHECK.forEach { (key, value) in
                            let markedResults = Self.mark(searchString: key,
                                                          in: fullFileName,
                                                          lineNumber: nil,
                                                          caseInsensitive: true,
                                                          requiredReasonKeys: [value])
                            guard let firstResult = markedResults.first?.1 else {
                                return
                            }
                            let highlightedCode = "\(Self.addBracketsToString(firstResult.line,around: firstResult.range))"
                            let foundInBuildPhase = "Found \(highlightedCode) in \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) Frameworks Build Phase."
                            self.updateRequiredAPIs(value,
                                                    with: PresentedResult(filePath: foundInBuildPhase))
                        }
                    })
                    self.concurrentStream.success(spinner: spinner,
                                                  "Parsed \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) Frameworks Build Phase")
                }))
            }

            var sourceFiles: [PBXFileElement] = []
            do {
                sourceFiles = try target.sourceFiles()
            }
            catch is PBXObjectError {
                // Suppress PBXObjectError
            }

            let spinner = concurrentStream.createSilentSpinner(with: "Parsing \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files...")
            concurrentStream.start(spinner: spinner)
            do {
                var filePathsForParsing: [Path] = []
                try sourceFiles.forEach { file in
                    guard let filePath = file.path,
                          let ext = Path(filePath).extension,
                          ALLOWED_EXTENSIONS.contains(ext)
                    else {
                        return
                    }
                    guard let fullPath = try file.fullPath(sourceRoot: path.parent()) else {
                        return
                    }
                    filePathsForParsing.append(fullPath)
                }
                try self.parseFiles(filePathsForParsing: filePathsForParsing,
                                    targetName: target.name,
                                    spinner: spinner)
                self.concurrentStream.success(spinner: spinner,
                                              "Parsed \(filePathsForParsing.count) \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files")
            }
            catch {
                self.concurrentStream.error(spinner: spinner,
                                            "Error parsing \(CliSyntaxColor.GREEN)\(target.name)'s\(CliSyntaxColor.END) source files: \(CliSyntaxColor.RED)\(error)\(CliSyntaxColor.END)")
            }
        }

        _ = group.wait(timeout: .distantFuture)
        concurrentStream.waitAndShowCursor()

        print("---")
    }
}


================================================
FILE: Sources/PrivacyManifest/main.swift
================================================
//
//  main.swift
//
//
//  Created by Stelios Petrakis on 9/4/24.
//

import Foundation

import ArgumentParser
import Spinner
import PathKit

struct PrivacyManifest: ParsableCommand {
    static let configuration = CommandConfiguration(
        commandName: "privacy-manifest",
        abstract: "Privacy Manifest tool",
        discussion: """
An easy and fast way to parse your whole Xcode project, Xcode workspace or
Swift Package in order to find whether your codebase makes use of Apple's
required reason APIs
(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).

!!! Disclaimer: This tool must *not* be used as the only way to generate the privacy manifest. Do your own research !!!
""",
        version: "0.0.18",
        subcommands: [Analyze.self])
}

struct Analyze: ParsableCommand {
    private static let PRIVACYINFO_FILENAME = "PrivacyInfo.xcprivacy"

    // The data structure of the generated PrivacyInfo.xcprivacy file
    struct PrivacyManifestDataStructure: Encodable {
        struct PrivacyAccessedAPIType: Encodable {
            var nsPrivacyAccessedAPIType: String
            var nSPrivacyAccessedAPITypeReasons: [String]

            enum CodingKeys: String, CodingKey {
                case nsPrivacyAccessedAPIType = "NSPrivacyAccessedAPIType"
                case nSPrivacyAccessedAPITypeReasons = "NSPrivacyAccessedAPITypeReasons"
            }
        }

        var nsPrivacyTracking: Bool
        var nsPrivacyTrackingDomains: [String]
        var nsPrivacyCollectedDataTypes: [[String:String]]
        var nsPrivacyAccessedAPITypes: [PrivacyAccessedAPIType]

        enum CodingKeys: String, CodingKey {
            case nsPrivacyTracking = "NSPrivacyTracking"
            case nsPrivacyTrackingDomains = "NSPrivacyTrackingDomains"
            case nsPrivacyCollectedDataTypes = "NSPrivacyCollectedDataTypes"
            case nsPrivacyAccessedAPITypes = "NSPrivacyAccessedAPITypes"
        }
    }

    enum DetectedProjectType {
        case xcodeProject(Path)
        case xcodeWorkspace(Path)
        case swiftPackage(Path)
        case directory(Path)
    }

    public static let configuration = CommandConfiguration(
        commandName: "analyze",
        abstract: "Analyzes the project to detect privacy aware API usage",
        discussion: """
Supports Xcode projects (.xcodeproj), Xcode workspaces (.xcworkspace) and
Swift Packages (Package.swift).

!!! Disclaimer: This tool must *not* be used as the only way to generate the privacy manifest. Do your own research !!!
"""
    )
    
    @Option(name: .long, help: """
Either the (relative/absolute) path to the project's
.xcodeproj(e.g. path/to/MyProject.xcodeproj),
.xcworkspace (e.g. path/to/MyWorkspace.xcworkspace) or
Package.swift (e.g. path/to/Package.swift).
""")
    private var project : String

    @Flag(name: .long, help: "Reveals the API occurrences on each file.")
    var revealOccurrences: Bool = false

    @Flag(name: .long, help: """
Look up the source files of library / framework targets.

By default, when a library / framework target is encountered, the source files
are not checked.

Warning: If specified, the process might take a while to complete based on the
amount of project / targets.
""")
    var deepLibraryFrameworkCheck: Bool = false

    @Option(name: .long, help: """
The path to the directory where the privacy manifest file will be generated (Optional).
""")
    var output: String?

    func run() throws {
        let projectPath = Path(project).absolute()
        var detectedProjectType: DetectedProjectType?

        if projectPath.lastComponent == PACKAGE_SWIFT_FILENAME {
            detectedProjectType = .swiftPackage(projectPath)
        }
        else if projectPath.extension == XCODE_PROJECT_PATH_EXTENSION {
            detectedProjectType = .xcodeProject(projectPath)
        }
        else if projectPath.extension == XCODE_WORKSPACE_PATH_EXTENSION {
            detectedProjectType = .xcodeWorkspace(projectPath)
        }
        else if projectPath.isDirectory {
            // Reverse sort the children paths so that xcworkspace is parsed
            // first if both .xcodeproj and .xcworkspace are found in the same
            // path.
            let children = try projectPath.children().sorted().reversed()
            guard children.count > 0 else {
                print("\(CliSyntaxColor.RED)Empty directory: \(projectPath)\(CliSyntaxColor.END)")
                return
            }
            children.forEach { childPath in
                if detectedProjectType != nil {
                    return
                }
                else if childPath.extension == XCODE_WORKSPACE_PATH_EXTENSION {
                    detectedProjectType = .xcodeWorkspace(childPath)
                }
                else if childPath.extension == XCODE_PROJECT_PATH_EXTENSION {
                    detectedProjectType = .xcodeProject(childPath)
                }
                else if childPath.lastComponent == PACKAGE_SWIFT_FILENAME {
                    detectedProjectType = .swiftPackage(childPath)
                }
            }
            if detectedProjectType == nil {
                detectedProjectType = .directory(projectPath)
            }
        }

        guard let detectedProjectType = detectedProjectType else {
            print("\(CliSyntaxColor.RED)File type not supported: \(projectPath)\(CliSyntaxColor.END)")
            return
        }

        var requiredAPIs: [RequiredReasonKey: Set<PresentedResult>]?

        do {
            switch detectedProjectType {
            case .swiftPackage(let path):
                print("Swift Package detected: \(CliSyntaxColor.WHITE_BOLD)\(path)\(CliSyntaxColor.END)")
                let swiftPackage = SwiftPackageProjectParser(with: path)
                try measure {
                    try swiftPackage.parse()
                }
                requiredAPIs = swiftPackage.process(revealOccurrences: revealOccurrences)
            case .xcodeProject(let path):
                print("Xcode project detected: \(CliSyntaxColor.WHITE_BOLD)\(path)\(CliSyntaxColor.END)")
                let xcodeProject = XcodeProjectParser(with: path,
                                                      deepLibraryFrameworkCheck: deepLibraryFrameworkCheck)
                try measure {
                    try xcodeProject.parse()
                }
                requiredAPIs = xcodeProject.process(revealOccurrences: revealOccurrences)
            case .xcodeWorkspace(let path):
                print("Xcode workspace detected: \(CliSyntaxColor.WHITE_BOLD)\(path)\(CliSyntaxColor.END)")
                let xcodeWorkspace = XcodeWorkspaceParser(with: path,
                                                          deepLibraryFrameworkCheck: deepLibraryFrameworkCheck)
                try measure {
                    try xcodeWorkspace.parse()
                }
                requiredAPIs = xcodeWorkspace.process(revealOccurrences: revealOccurrences)
            case .directory(let path):
                print("Directory detected: \(CliSyntaxColor.WHITE_BOLD)\(path)\(CliSyntaxColor.END)")
                let xcodeProject = DirectoryProjectParser(with: path)
                try measure {
                    try xcodeProject.parse()
                }
                requiredAPIs = xcodeProject.process(revealOccurrences: revealOccurrences)
            }
        }
        catch {
            ConcurrentSpinnerStream.showCursor()
            print("\n\(CliSyntaxColor.RED)✖ Parser Error: \(error)\(CliSyntaxColor.END)")
            return
        }

        if let output = output,
           let requiredAPIs = requiredAPIs {
            print("---")

            let outputPath = Path(output)

            guard outputPath.isDirectory else {
                print("\(CliSyntaxColor.RED)Error: Output path not a directory\(CliSyntaxColor.END)")
                return
            }
            generateManifest(requiredAPIs,
                             outputPath: Path(output) + Self.PRIVACYINFO_FILENAME)
        }
    }

    func measure(function: () throws -> Void) throws {
        let clock = ContinuousClock()
        let result = try clock.measure(function)
        print("Execution took \(result)")
    }

    func generateManifest(_ requiredAPIs: [RequiredReasonKey: Set<PresentedResult>],
                          outputPath: Path) {
        var manifestReasons: [PrivacyManifestDataStructure.PrivacyAccessedAPIType] = []

        // Show the THIRD_PARTY_SDK_KEY first
        requiredAPIs.sorted {
            if $0.key == .THIRD_PARTY_SDK_KEY {
                return true
            }
            else if $1.key == .THIRD_PARTY_SDK_KEY {
                return false
            }
            else {
                return $0.key < $1.key
            }
        }.forEach { (key, value) in
            guard value.count > 0, key.reasons.count > 0 else {
                return
            }

            if key == .THIRD_PARTY_SDK_KEY, let reason = key.reasons.first {
                var results: Set<String> = Set()
                value.forEach { result in
                    results.update(with: result.filePath)
                }
                print("\n\(CliSyntaxColor.WHITE_BOLD)WARNING:\(CliSyntaxColor.END) The following third-party SDKs were detected:\n")
                print("* \(results.joined(separator: "\n* "))")
                print(reason.value)
                print("\(CliSyntaxColor.CYAN)⚓︎ \(key.link)\(CliSyntaxColor.END)\n")
                print("ENTER to continue", terminator: "")
                _ = readLine()
                return
            }

            guard let privacyManifestKey = key.privacyManifestKey else {
                return
            }

            print("\n\(CliSyntaxColor.WHITE_BOLD)\(value.count) \(value.count == 1 ? "occurrence" : "occurrences") for \(key.description)\(CliSyntaxColor.END). Available reasons:\n")

            var index = 0
            let reasonKeys = [String](key.reasons.keys)
            reasonKeys.forEach { reasonKey in
                guard let value = key.reasons[reasonKey] else {
                    return
                }
                print("""
\(CliSyntaxColor.WHITE_BOLD)\(index+1).\(CliSyntaxColor.END) \(value)\n
""")
                index += 1
            }

            print("\(CliSyntaxColor.CYAN)⚓︎ \(key.link)\(CliSyntaxColor.END)\n")

            print("Enter values \(1)-\(reasonKeys.count) that match your case (comma separated for multiple values, ENTER for none): ",
                  terminator: "")

            var manifestReasonKeys: [String] = []

            if let input = readLine() {
                let values = input.components(separatedBy: ",")
                values.forEach { value in
                    guard let index = Int(value.trimmingCharacters(in: .whitespaces)),
                        index - 1 >= 0 && index - 1 < reasonKeys.count else {
                        return
                    }
                    let reasonKey = reasonKeys[index - 1]
                    manifestReasonKeys.append(reasonKey)
                }
            }

            if manifestReasonKeys.count > 0 {
                manifestReasons.append(PrivacyManifestDataStructure.PrivacyAccessedAPIType(
                    nsPrivacyAccessedAPIType: privacyManifestKey,
                    nSPrivacyAccessedAPITypeReasons: manifestReasonKeys))
            }
        }

        print("\n")

        guard manifestReasons.count > 0 else {
            print("\(CliSyntaxColor.YELLOW)No reasons were provided, Privacy Manifest file generation was skipped.\(CliSyntaxColor.END)")
            return
        }

        let privacyManifestDataStructure = PrivacyManifestDataStructure(
            nsPrivacyTracking: false,
            nsPrivacyTrackingDomains: [],
            nsPrivacyCollectedDataTypes: [],
            nsPrivacyAccessedAPITypes: manifestReasons)

        do {
            try PropertyListEncoder().encode(privacyManifestDataStructure).write(to: outputPath.url)
            print("\(CliSyntaxColor.GREEN)✔\(CliSyntaxColor.END) Privacy Manifest file was generated successfully at \(CliSyntaxColor.WHITE_BOLD)\(outputPath.absolute())\(CliSyntaxColor.END)")
            print("""
\(CliSyntaxColor.YELLOW_BRIGHT)
⚠--------------------------- Disclaimer ---------------------------+
| Check the values of the generated Privacy Manifest file before   |
| submitting your application to the App Store for review, as only |
| the values of the NSPrivacyAccessedAPITypes dictionary are based |
| on your responses.                                               |
+------------------------------------------------------------------+
\(CliSyntaxColor.END)
""")
        } catch {
            print("\(CliSyntaxColor.RED)✖ Error generating Privacy Manifest file: \(error)\(CliSyntaxColor.END)")
        }
    }
}

PrivacyManifest.main()
Download .txt
gitextract_3gviv89a/

├── .gitignore
├── LICENSE
├── Makefile
├── Package.resolved
├── Package.swift
├── README.md
└── Sources/
    └── PrivacyManifest/
        ├── Constants.swift
        ├── DirectoryProjectParser.swift
        ├── ProjectParser.swift
        ├── SpinnerStreams.swift
        ├── SwiftPackageProjectParser.swift
        ├── XcodeProjectParser.swift
        └── main.swift
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (92K chars).
[
  {
    "path": ".gitignore",
    "chars": 63,
    "preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n.swiftpm/\n"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Makefile",
    "chars": 108,
    "preview": "install:\n\t\tswift build -c release\n\t\tinstall .build/release/privacy-manifest /usr/local/bin/privacy-manifest\n"
  },
  {
    "path": "Package.resolved",
    "chars": 5183,
    "preview": "{\n  \"pins\" : [\n    {\n      \"identity\" : \"aexml\",\n      \"kind\" : \"remoteSourceControl\",\n      \"location\" : \"https://githu"
  },
  {
    "path": "Package.swift",
    "chars": 1223,
    "preview": "// swift-tools-version:5.9\n// The swift-tools-version declares the minimum version of Swift required to build this packa"
  },
  {
    "path": "README.md",
    "chars": 3636,
    "preview": "# Privacy Manifest\n\n<p align=\"left\">\n<img src=\"https://img.shields.io/badge/macOS_v13%2B-_?style=flat&logo=apple&label=p"
  },
  {
    "path": "Sources/PrivacyManifest/Constants.swift",
    "chars": 20953,
    "preview": "//\n//  Constants.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nstruct CliSyntaxColor {\n"
  },
  {
    "path": "Sources/PrivacyManifest/DirectoryProjectParser.swift",
    "chars": 1691,
    "preview": "//\n//  DirectoryProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 15/4/24.\n//\n\nimport Foundation\n\nimport Pa"
  },
  {
    "path": "Sources/PrivacyManifest/ProjectParser.swift",
    "chars": 8363,
    "preview": "//\n//  ProjectParser.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\nimpor"
  },
  {
    "path": "Sources/PrivacyManifest/SpinnerStreams.swift",
    "chars": 5357,
    "preview": "//\n//  SpinnerStreams.swift\n//\n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinner\n\n// "
  },
  {
    "path": "Sources/PrivacyManifest/SwiftPackageProjectParser.swift",
    "chars": 6187,
    "preview": "//\n//  SwiftPackageProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport"
  },
  {
    "path": "Sources/PrivacyManifest/XcodeProjectParser.swift",
    "chars": 10005,
    "preview": "//\n//  XcodeProjectParser.swift\n//  \n//\n//  Created by Stelios Petrakis on 14/4/24.\n//\n\nimport Foundation\n\nimport Spinne"
  },
  {
    "path": "Sources/PrivacyManifest/main.swift",
    "chars": 13113,
    "preview": "//\n//  main.swift\n//\n//\n//  Created by Stelios Petrakis on 9/4/24.\n//\n\nimport Foundation\n\nimport ArgumentParser\nimport S"
  }
]

About this extraction

This page contains the full source code of the stelabouras/privacy-manifest GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (85.2 KB), approximately 19.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!