cocoapods-privacy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 75f110acdd889169f011bfa5508d1941cdaccf78ce5dcb43bc51dd79236e793f
4
+ data.tar.gz: 9db44f1ca04a5991000862935336ba4d1ebbb96508c2e6b2d0877a5d21f5f1d5
5
+ SHA512:
6
+ metadata.gz: adca9245bb2e2feef01acf8f5215f933b3bd89a97cfd279494e9d0c0f6edc92b82d2daf1b86e8b439e5c7ae1ce0746cb3b67100fdcc23c390ee339bee56d8d60
7
+ data.tar.gz: a313ab80c11f3479da5f173de9179aae6498d677289e05e0674ffecb089aa6c7905856c061539a5cb1d9add0c418959362edef520a14c2386566ef64fb11a1cc
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2024 youhui <youhui@babybus.com>
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # cocoapods-privacy
2
+
3
+ A description of cocoapods-privacy.
4
+
5
+ ## Installation
6
+
7
+ $ gem install cocoapods-privacy
8
+
9
+ ## Usage
10
+
11
+ $ pod spec privacy POD_NAME
@@ -0,0 +1,34 @@
1
+ require 'cocoapods-privacy/command'
2
+
3
+ module Pod
4
+ class Config
5
+ attr_accessor :privacy_folds
6
+ attr_accessor :is_privacy
7
+ end
8
+ end
9
+
10
+ module Pod
11
+ class Command
12
+ class Install < Command
13
+ class << self
14
+ alias_method :origin_options, :options
15
+ def options
16
+ [
17
+ ['--privacy', '使用该参数,会自动生成并更新PrivacyInfo.xcprivacy'],
18
+ ['--privacy-folds=folds', '指定文件夹检索,多个文件夹使用逗号","分割'],
19
+ ].concat(origin_options)
20
+ end
21
+ end
22
+
23
+ alias_method :privacy_origin_initialize, :initialize
24
+ def initialize(argv)
25
+ privacy_folds = argv.option('privacy-folds', '').split(',')
26
+ is_privacy = argv.flag?('privacy',false)
27
+ privacy_origin_initialize(argv)
28
+ instance = Pod::Config.instance
29
+ instance.privacy_folds = privacy_folds
30
+ instance.is_privacy = is_privacy
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ require 'cocoapods-privacy/command'
2
+
3
+ module Pod
4
+ class Command
5
+ class Privacy < Command
6
+ class Config < Privacy
7
+ self.summary = '初始化隐私清单配置'
8
+
9
+ self.description = <<-DESC
10
+ 初始化隐私清单配置,包含必须的隐私api模版,和source 黑白名单等,配置文件格式详细见 #{"https://github.com/ymoyao/cocoapods-privacy"}
11
+ DESC
12
+
13
+ def initialize(argv)
14
+ @config = argv.shift_argument
15
+ super
16
+ end
17
+
18
+ def validate!
19
+ super
20
+ help! 'A config url is required.' unless @config
21
+ raise Informative, "配置文件格式不是 JSON,请检查配置#{@config}" unless @config.end_with?(".json")
22
+ end
23
+
24
+ def run
25
+ load_config_file()
26
+ end
27
+
28
+ def load_config_file
29
+ # 检查 @config 是远程 URL 还是本地文件路径
30
+ if @config.start_with?('http')
31
+ download_remote_config
32
+ else
33
+ copy_local_config
34
+ end
35
+ end
36
+
37
+ def download_remote_config
38
+ # 配置文件目录
39
+ cache_config_file = PrivacyUtils.cache_config_file
40
+
41
+ # 开始下载
42
+ system("curl -o #{cache_config_file} #{@config}")
43
+
44
+ if File.exist?(cache_config_file)
45
+ puts "配置文件已下载到: #{cache_config_file}"
46
+ else
47
+ raise Informative, "配置文件下载出错,请检查下载地址#{@config}"
48
+ end
49
+ end
50
+
51
+ def copy_local_config
52
+ # 配置文件目录
53
+ cache_config_file = PrivacyUtils.cache_config_file
54
+
55
+ # 复制本地文件
56
+ FileUtils.cp(@config, cache_config_file)
57
+
58
+ puts "配置文件已复制到: #{cache_config_file}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
@@ -0,0 +1,36 @@
1
+ module Pod
2
+ class Command
3
+ class Privacy < Command
4
+ class Install < Privacy
5
+ self.summary = '在工程中创建对应隐私清单文件'
6
+
7
+ self.description = <<-DESC
8
+ 1、在工程Resources 文件夹下创建隐私清单文件
9
+ 2、搜索对应组件,补全隐私Api部分
10
+ 3、只处理隐私Api部分,隐私权限相关需要自行处理!!!
11
+ DESC
12
+
13
+ def self.options
14
+ [
15
+ ["--folds=folds", '传入自定义搜索文件夹,多个文件目录使用“,”分割'],
16
+ ].concat(super)
17
+ end
18
+
19
+ def initialize(argv)
20
+ @folds = argv.option('folds', '').split(',')
21
+ super
22
+ end
23
+
24
+ def run
25
+ installer = installer_for_config
26
+ installer.repo_update = false
27
+ installer.update = false
28
+ installer.deployment = false
29
+ installer.clean_install = false
30
+ installer.privacy_analysis(@folds)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,27 @@
1
+ module Pod
2
+ class Command
3
+ class Privacy < Command
4
+ class Spec < Privacy
5
+ self.summary = '根据 podspec 创建对应隐私清单文件'
6
+
7
+ self.description = <<-DESC
8
+ 根据podspec 创建对应隐私清单文件,并自动修改podspec文件,以映射对应隐私清单文件。
9
+ DESC
10
+
11
+ self.arguments = [
12
+ CLAide::Argument.new('podspec_file', false, true),
13
+ ]
14
+
15
+ def initialize(argv)
16
+ @podspec_file = argv.arguments!.first
17
+ super
18
+ end
19
+
20
+ def run
21
+ PrivacyModule.load_module(@podspec_file)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,20 @@
1
+ module Pod
2
+ class Command
3
+ class Privacy < Command
4
+
5
+ def initialize(argv)
6
+ super
7
+ end
8
+
9
+ def run
10
+ if PrivacyUtils.isMainProject
11
+ puts "检测到#{PrivacyUtils.project_path || ""}工程文件, 请使用 pod privacy install 对工程进行隐私清单创建和自动检索"
12
+ elsif PrivacyUtils.podspec_file_path
13
+ puts "检测到#{PrivacyUtils.podspec_file_path || ""} 组件, 请使用 pod privacy spec 对组件进行隐私清单创建和自动检索"
14
+ else
15
+ puts "未检测到工程或podspec 文件, 请切换到工程或podspec文件目录下再次执行命令"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require 'cocoapods-privacy/command/privacy'
2
+ require 'cocoapods-privacy/command/privacy/config'
3
+ require 'cocoapods-privacy/command/privacy/install'
4
+ require 'cocoapods-privacy/command/privacy/spec'
5
+ require 'cocoapods-privacy/command/install'
6
+
7
+ require 'cocoapods-privacy/privacy/privacy_specification_hook'
8
+ require 'cocoapods-privacy/privacy/privacy_installer_hook'
9
+ require 'cocoapods-privacy/privacy/PrivacyUtils'
10
+ require 'cocoapods-privacy/privacy/PrivacyModule'
11
+ require 'cocoapods-privacy/privacy/PrivacyHunter'
12
+ require 'cocoapods-privacy/privacy/PrivacyConfig'
13
+
@@ -0,0 +1,3 @@
1
+ module CocoapodsPrivacy
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'cocoapods-privacy/command'
2
+
3
+ module Privacy
4
+ class Config
5
+
6
+ def initialize()
7
+ config_content = File.read(PrivacyUtils.cache_config_file)
8
+ @json = JSON.parse(config_content)
9
+ end
10
+
11
+ def api_template_url
12
+ return @json['api.template.url'] || ""
13
+ end
14
+
15
+ def source_black_list
16
+ return @json['source.black.list'] || []
17
+ end
18
+
19
+ def source_white_list
20
+ return @json['source.white.list'] || []
21
+ end
22
+
23
+ def self.instance
24
+ @instance ||= new
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,180 @@
1
+ require 'json'
2
+ require 'cocoapods-privacy/command'
3
+
4
+ ##
5
+ # 功能介绍:
6
+ # 1、检测本地隐私协议清单模版是否最新,如果不存在或不是最新,那么下载远端隐私协议模版
7
+ # 2、使用模版对相关文件夹进行检索
8
+ # 3、检索到的内容转换成隐私协议格式写入 隐私清单文件 PrivacyInfo.xcprivacy
9
+ ##
10
+ module PrivacyHunter
11
+ KTypes = "NSPrivacyAccessedAPITypes"
12
+ KType = "NSPrivacyAccessedAPIType"
13
+ KReasons = "NSPrivacyAccessedAPITypeReasons"
14
+ KAPI = "NSPrivacyAccessedAPI"
15
+
16
+ # source_files = ARGV[0]#传入source文件路径,如有多个使用 “,” 逗号分割
17
+ # privacyInfo_file = ARGV[1]#传入目标 PrivacyInfo.xcprivacy
18
+
19
+ def self.search_pricacy_apis(source_folders)
20
+ # #读取源文件,也就是搜索目标文件
21
+ # source_folders = source_files.split(",")
22
+ #模版数据源plist文件
23
+ template_plist_file = fetch_template_plist_file()
24
+
25
+ # 读取并解析 数据源 plist 文件
26
+ json_str = `plutil -convert json -o - "#{template_plist_file}"`.chomp
27
+ map = JSON.parse(json_str)
28
+ arr = map[KTypes]
29
+
30
+ #解析并按照API模版查询指定文件夹
31
+ privacyArr = []
32
+ arr.each do |value|
33
+ privacyDict = {}
34
+ type = value[KType]
35
+ reasons = []
36
+ apis = value[KAPI]
37
+ apis.each do |s_key, s_value|
38
+ if search_files(source_folders, s_key)
39
+ s_vlaue_split = s_value.split(',')
40
+ reasons += s_vlaue_split
41
+ end
42
+ end
43
+
44
+ #按照隐私清单拼接数据
45
+ reasons = reasons.uniq
46
+ if !reasons.empty?
47
+ privacyDict[KType] = type
48
+ privacyDict[KReasons] = reasons
49
+ privacyArr.push(privacyDict)
50
+ end
51
+ # puts "type: #{type}"
52
+ # puts "reasons: #{reasons.uniq}"
53
+
54
+ end
55
+
56
+ # 打印出搜索结果
57
+ puts privacyArr
58
+
59
+ # 转换成 JSON 字符串
60
+ json_data = privacyArr.to_json
61
+ end
62
+
63
+
64
+ def self.write_to_privacy(json_data,privacy_path)
65
+ # 转换 JSON 为 plist 格式
66
+ plist_data = `echo '#{json_data}' | plutil -convert xml1 - -o -`
67
+
68
+ # 创建临时文件
69
+ temp_plist = File.join(PrivacyUtils.cache_privacy_fold,"#{PrivacyUtils.to_md5(privacy_path)}.plist")
70
+ File.write(temp_plist, plist_data)
71
+
72
+ # 获取原先文件中的 NSPrivacyAccessedAPITypes 数据
73
+ origin_privacy_data = `/usr/libexec/PlistBuddy -c 'Print :NSPrivacyAccessedAPITypes' '#{privacy_path}' 2>/dev/null`
74
+ new_privacy_data = `/usr/libexec/PlistBuddy -c 'Print' '#{temp_plist}'`
75
+
76
+ # 检查新数据和原先数据是否一致
77
+ if origin_privacy_data.strip == new_privacy_data.strip
78
+ puts "NSPrivacyAccessedAPITypes 数据一致,无需插入。"
79
+ else
80
+ unless origin_privacy_data.strip.empty?
81
+ # 删除 :NSPrivacyAccessedAPITypes 键
82
+ system("/usr/libexec/PlistBuddy -c 'Delete :NSPrivacyAccessedAPITypes' '#{privacy_path}'")
83
+ end
84
+
85
+ # 添加 :NSPrivacyAccessedAPITypes 键并设置为数组
86
+ system("/usr/libexec/PlistBuddy -c 'Add :NSPrivacyAccessedAPITypes array' '#{privacy_path}'")
87
+
88
+ # 合并 JSON 数据到隐私文件
89
+ system("/usr/libexec/PlistBuddy -c 'Merge #{temp_plist} :NSPrivacyAccessedAPITypes' '#{privacy_path}'")
90
+
91
+ puts "NSPrivacyAccessedAPITypes 数据已插入。"
92
+ end
93
+
94
+ # 删除临时文件
95
+ File.delete(temp_plist)
96
+
97
+ end
98
+
99
+
100
+ private
101
+
102
+
103
+ def self.fetch_template_plist_file
104
+
105
+ unless File.exist?(PrivacyUtils.cache_config_file)
106
+ raise Informative, "无配置文件,run `pod privacy config config_file' 进行配置"
107
+ end
108
+
109
+ template_url = Privacy::Config.instance.api_template_url
110
+ unless template_url && !template_url.empty?
111
+ raise Informative, "配置文件中无 `api.template.url` 配置,请补全后再更新配置 `pod privacy config config_file` "
112
+ end
113
+
114
+ # 目标文件路径
115
+ local_file_path = File.join(PrivacyUtils.cache_privacy_fold, 'NSPrivacyAccessedAPITypes.plist')
116
+
117
+ # 获取远程文件更新时间
118
+ remote_file_time = remoteFileTime?(template_url)
119
+
120
+ # 判断本地文件的最后修改时间是否与远端文件一致,如果一致则不进行下载
121
+ if File.exist?(local_file_path) && file_identical?(local_file_path, remote_file_time)
122
+ puts "本地文件与远端文件一致,无需下载。文件路径: #{local_file_path}"
123
+ else
124
+ # 使用 curl 下载文件
125
+ system("curl -o #{local_file_path} #{template_url}")
126
+ puts "文件已下载到: #{local_file_path}"
127
+
128
+ # 同步远程文件时间到本地文件
129
+ syncFileTime?(local_file_path,remote_file_time)
130
+ end
131
+
132
+ local_file_path
133
+ end
134
+
135
+ # 获取远程文件更新时间
136
+ def self.remoteFileTime?(remote_url)
137
+ uri = URI.parse(remote_url)
138
+ http = Net::HTTP.new(uri.host, uri.port)
139
+ http.use_ssl = (uri.scheme == 'https')
140
+ response = http.request_head(uri.path)
141
+
142
+ response['Last-Modified']
143
+ end
144
+
145
+ # 判断本地文件的最后修改时间与远端文件的最后修改时间是否一致
146
+ def self.file_identical?(local_file_path, remote_file_time)
147
+ remote_file_time && Time.parse(remote_file_time) == File.mtime(local_file_path)
148
+ end
149
+
150
+ # 同步远程文件时间到本地文件
151
+ def self.syncFileTime?(local_file_path, remote_file_time)
152
+ File.utime(File.atime(local_file_path), Time.parse(remote_file_time), local_file_path)
153
+ end
154
+
155
+ # 文件是否包含内容
156
+ def self.contains_keyword?(file_path, keyword)
157
+ File.read(file_path).include? keyword
158
+ end
159
+
160
+ #搜索所有子文件夹
161
+ def self.search_files(folder_paths, keyword)
162
+
163
+ # 获取文件夹下所有文件(包括子文件夹)
164
+ all_files = []
165
+ folder_paths.each do |folder|
166
+ allowed_extensions = ['m', 'c', 'swift', 'mm', 'hap', 'cpp']
167
+ pattern = File.join(folder, '**', '*.{'+allowed_extensions.join(',')+'}')
168
+ all_files += Dir.glob(pattern, File::FNM_DOTMATCH).reject { |file| File.directory?(file) }
169
+ end
170
+ # 遍历文件进行检索
171
+ all_files.uniq.each_with_index do |file_path, index|
172
+ if contains_keyword?(file_path, keyword)
173
+ puts "File #{file_path} contains the keyword '#{keyword}'."
174
+ return true
175
+ end
176
+ end
177
+ return false
178
+ end
179
+ end
180
+
@@ -0,0 +1,296 @@
1
+ require 'cocoapods-privacy/command'
2
+ require 'cocoapods-core/specification/dsl/attribute_support'
3
+ require 'cocoapods-core/specification/dsl/attribute'
4
+ require 'xcodeproj'
5
+
6
+ class BBRow
7
+ attr_accessor :content, :is_comment, :is_spec_start, :is_spec_end, :key, :value
8
+
9
+ def initialize(content, is_comment=false, is_spec_start=false, is_spec_end=false)
10
+ @content = content
11
+ @is_comment = is_comment
12
+ @is_spec_start = is_spec_start
13
+ @is_spec_end = is_spec_end
14
+
15
+ parse_key_value
16
+ end
17
+
18
+ def parse_key_value
19
+ # 在这里添加提取 key 和 value 的逻辑
20
+ if @content.include?('=')
21
+ key_value_split = @content.split('=')
22
+ @key = key_value_split[0]
23
+ @value = key_value_split[1..-1].join('=')
24
+ else
25
+ @key = nil
26
+ @value = nil
27
+ end
28
+ end
29
+ end
30
+
31
+ class BBSpec
32
+ attr_accessor :name, :alias_name, :full_name, :rows, :privacy_sources, :privacy_file
33
+
34
+ def initialize(name,alias_name,full_name)
35
+ @rows = []
36
+ @privacy_sources = []
37
+ @name = name
38
+ @alias_name = alias_name
39
+ @full_name = full_name
40
+ @privacy_file = "Pod/Privacy/#{full_name}/PrivacyInfo.xcprivacy"
41
+ end
42
+
43
+ def privacy_handle(podspec_file_path)
44
+ @rows.each_with_index do |line, index|
45
+ if !line || line.is_a?(BBSpec) || !line.key || line.key.empty?
46
+ next
47
+ end
48
+
49
+ if !line.is_comment && line.key.include?(".resource_bundle")
50
+ @has_resource_bundle = true
51
+ elsif !line.is_comment && line.key.include?(".source_files")
52
+ spec = eval("Pod::Spec.new do |s|; s.source_files = #{line.value}; end;")
53
+ if spec && !spec.attributes_hash['source_files'].nil?
54
+ source_files_value = spec.attributes_hash['source_files']
55
+ if source_files_value.is_a?(String)
56
+ source_files_array = [source_files_value]
57
+ elsif source_files_value.is_a?(Array)
58
+ # 如果已经是数组,直接使用
59
+ source_files_array = source_files_value
60
+ else
61
+ # 其他情况,默认为空数组
62
+ source_files_array = []
63
+ end
64
+
65
+ @privacy_sources = source_files_array.map do |file_path|
66
+ File.join(File.dirname(podspec_file_path), file_path.strip)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ create_privacy_file_if_need(podspec_file_path)
72
+ modify_privacy_resource_bundle_if_need()
73
+ end
74
+
75
+ # 对应Spec新增隐私文件
76
+ def create_privacy_file_if_need(podspec_file_path)
77
+ if !@privacy_sources.empty?
78
+ PrivacyUtils.create_privacy_if_empty(File.join(File.dirname(podspec_file_path), @privacy_file))
79
+ end
80
+ end
81
+
82
+ # 把新增的隐私文件 映射给 podspec
83
+ def modify_privacy_resource_bundle_if_need
84
+ if !@privacy_sources.empty?
85
+ privacy_resource_bundle = { "#{full_name}.privacy" => @privacy_file }
86
+ if @has_resource_bundle
87
+ @rows.each_with_index do |line, index|
88
+ if !line || line.is_a?(BBSpec) || !line.key || line.key.empty?
89
+ next
90
+ end
91
+
92
+ if !line.is_comment && line.key.include?(".resource_bundle")
93
+ origin_resource_bundle = eval(line.value)
94
+ merged_resource_bundle = origin_resource_bundle.merge(privacy_resource_bundle)
95
+
96
+ @resource_bundle = merged_resource_bundle
97
+ line.value = merged_resource_bundle
98
+ line.content = "#{line.key}= #{line.value}"
99
+ end
100
+ end
101
+ else
102
+ space = PrivacyUtils.count_spaces_before_first_character(rows.first.content)
103
+ line = "#{alias_name}.resource_bundle = #{privacy_resource_bundle}"
104
+ line = PrivacyUtils.add_spaces_to_string(line,space + 2)
105
+ row = BBRow.new(line)
106
+ @rows.insert(1, row)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ module PrivacyModule
114
+
115
+ public
116
+
117
+ # 处理工程
118
+ def self.load_project(folds)
119
+ project_path = PrivacyUtils.project_path()
120
+ resources_folder_path = File.join(File.basename(project_path, File.extname(project_path)),'Resources')
121
+ privacy_file_path = File.join(resources_folder_path,PrivacyUtils.privacy_name)
122
+ # 如果隐私文件不存在,创建隐私协议模版
123
+ unless File.exist?(privacy_file_path)
124
+ PrivacyUtils.create_privacy_if_empty(privacy_file_path)
125
+ end
126
+
127
+ # 如果没有隐私文件,那么新建一个添加到工程中
128
+ # 打开 Xcode 项目,在Resources 下创建
129
+ project = Xcodeproj::Project.open(File.basename(project_path))
130
+ main_group = project.main_group
131
+ resources_group = PrivacyUtils.find_group_by_path(main_group,resources_folder_path)
132
+ if resources_group.nil?
133
+ resources_group = main_group.new_group('Resources',resources_folder_path)
134
+ end
135
+
136
+ # 如果不存在引用,创建新的引入xcode引用
137
+ if resources_group.find_file_by_path(PrivacyUtils.privacy_name).nil?
138
+ resources_group.new_reference(PrivacyUtils.privacy_name)
139
+ # resources_group.new_file(privacy_file_path)
140
+ end
141
+
142
+ project.save
143
+
144
+ # 开始检索api,并返回json 字符串数据
145
+ json_data = PrivacyHunter.search_pricacy_apis(folds)
146
+
147
+ # 将数据写入隐私清单文件
148
+ PrivacyHunter.write_to_privacy(json_data,privacy_file_path)
149
+ end
150
+
151
+ # 处理组件
152
+ def self.load_module(podspec_file)
153
+ podspec_file_path = podspec_file ? podspec_file : PrivacyUtils.podspec_file_path
154
+ unless podspec_file_path && !podspec_file_path.empty?
155
+ raise Informative, "no podspec file were found, please run `pod privacy podspec_file_path`"
156
+ end
157
+
158
+ privacy_hash = PrivacyModule.check(podspec_file_path)
159
+ privacy_hash.each do |privacy_file_path, source_files|
160
+ data = PrivacyHunter.search_pricacy_apis(source_files)
161
+ PrivacyHunter.write_to_privacy(data,privacy_file_path) unless data.empty?
162
+ end
163
+ end
164
+
165
+ def self.check(podspec_file_path)
166
+ # Step 1: 读取podspec
167
+ lines = read_podspec(podspec_file_path)
168
+
169
+ # Step 2: 逐行解析并转位BBRow 模型
170
+ rows = parse_row(lines)
171
+
172
+ # Step 3.1:如果Row 是属于Spec 内,那么聚拢成BBSpec,
173
+ # Step 3.2:BBSpec 内使用数组存储其Spec 内的行
174
+ # Step 3.3 在合适位置给每个有效的spec都创建一个 隐私模版,并修改其podspec 引用
175
+ combin_sepcs_and_rows = combin_sepc_if_need(rows,podspec_file_path)
176
+
177
+ # Step 4: 展开修改后的Spec,重新转换成 BBRow
178
+ rows = unfold_sepc_if_need(combin_sepcs_and_rows)
179
+
180
+ # Step 5: 打开隐私模版,并修改其podspec文件,并逐行写入
181
+ File.open(podspec_file_path, 'w') do |file|
182
+ # 逐行写入 rows
183
+ rows.each do |row|
184
+ file.puts(row.content)
185
+ end
186
+ end
187
+
188
+
189
+ # Step 6: 获取privacy 相关信息,传递给后续处理
190
+ privacy_hash = fetch_privacy_hash(combin_sepcs_and_rows,podspec_file_path)
191
+ filtered_privacy_hash = privacy_hash.reject { |_, value| value.empty? }
192
+ filtered_privacy_hash
193
+ end
194
+
195
+ private
196
+ def self.read_podspec(file_path)
197
+ File.readlines(file_path)
198
+ end
199
+
200
+ def self.parse_row(lines)
201
+ rows = []
202
+ if_stack = [] #排除if end 干扰
203
+ lines.each do |line|
204
+ content = line.strip
205
+ is_comment = content.start_with?('#')
206
+ is_spec_start = !is_comment && (content.include?('Pod::Spec.new') || content.include?('.subspec'))
207
+ is_if = !is_comment && content.start_with?('if')
208
+ is_end = !is_comment && content.start_with?('end')
209
+ # 排除if end 对spec_end 的干扰
210
+ if_stack.push(true) if is_if
211
+ is_spec_end = if_stack.empty? && is_end
212
+ if_stack.pop if is_end
213
+ row = BBRow.new(line, is_comment, is_spec_start, is_spec_end)
214
+ rows << row
215
+ end
216
+ rows
217
+ end
218
+
219
+ # 数据格式:
220
+ # [
221
+ # BBRow
222
+ # BBRow
223
+ # BBSpec
224
+ # rows
225
+ # [
226
+ # BBRow
227
+ # BBSpec
228
+ # BBRow
229
+ # BBRow
230
+ # ]
231
+ # BBRow
232
+ # ......
233
+ # ]
234
+ # 合并Row -> Spec(会存在部分行不在Spec中:Spec new 之前的注释)
235
+ def self.combin_sepc_if_need(rows,podspec_file_path)
236
+ spec_stack = []
237
+ result_rows = []
238
+ default_name = File.basename(podspec_file_path, File.extname(podspec_file_path))
239
+
240
+ rows.each do |row|
241
+ if row.is_spec_start
242
+ # 创建 spec
243
+ name = row.content.split("'")[1]&.strip || default_name
244
+ alias_name = row.content.split("|")[1]&.strip
245
+ full_name = spec_stack.empty? ? name : "#{spec_stack.last.full_name}.#{name}"
246
+ spec = BBSpec.new(name,alias_name,full_name)
247
+ spec.rows << row
248
+
249
+ # 当存在 spec 时,存储在 spec.rows 中;不存在时,直接存储在外层
250
+ (spec_stack.empty? ? result_rows : spec_stack.last.rows) << spec
251
+
252
+ # spec 入栈
253
+ spec_stack.push(spec)
254
+ elsif row.is_spec_end
255
+ # 当前 spec 的 rows 加入当前行
256
+ spec_stack.last&.rows << row
257
+
258
+ #执行隐私协议修改
259
+ spec_stack.last.privacy_handle(podspec_file_path)
260
+
261
+ # spec 出栈
262
+ spec_stack.pop
263
+ else
264
+ # 当存在 spec 时,存储在 spec.rows 中;不存在时,直接存储在外层
265
+ (spec_stack.empty? ? result_rows : spec_stack.last.rows) << row
266
+ end
267
+ end
268
+
269
+ result_rows
270
+ end
271
+
272
+ # 把所有的spec中的rows 全部展开,拼接成一级数组【BBRow】
273
+ def self.unfold_sepc_if_need(rows)
274
+ result_rows = []
275
+ rows.each do |row|
276
+ if row.is_a?(BBSpec)
277
+ result_rows += unfold_sepc_if_need(row.rows)
278
+ else
279
+ result_rows << row
280
+ end
281
+ end
282
+ result_rows
283
+ end
284
+
285
+
286
+ def self.fetch_privacy_hash(rows,podspec_file_path)
287
+ privacy_hash = {}
288
+ filtered_rows = rows.select { |row| row.is_a?(BBSpec) }
289
+ filtered_rows.each do |spec|
290
+ privacy_hash[File.join(File.dirname(podspec_file_path),spec.privacy_file)] = spec.privacy_sources
291
+ privacy_hash.merge!(fetch_privacy_hash(spec.rows,podspec_file_path))
292
+ end
293
+ privacy_hash
294
+ end
295
+
296
+ end
@@ -0,0 +1,124 @@
1
+ require 'digest'
2
+
3
+ module PrivacyUtils
4
+
5
+ def self.privacy_name
6
+ 'PrivacyInfo.xcprivacy'
7
+ end
8
+
9
+ # 通过是否包含podspec 来判断是否为主工程
10
+ def self.isMainProject
11
+ !(podspec_file_path && !podspec_file_path.empty)
12
+ end
13
+
14
+ # 查找podspec
15
+ def self.podspec_file_path
16
+ base_path = Pathname.pwd
17
+ matching_files = Dir.glob(File.join(base_path, '*.podspec'))
18
+ matching_files.first
19
+ end
20
+
21
+ # xcode工程地址
22
+ def self.project_path
23
+ matching_files = Dir[File.join(Pathname.pwd, '*.xcodeproj')].uniq
24
+ matching_files.first
25
+ end
26
+
27
+ # xcode工程主代码目录
28
+ def self.project_code_fold
29
+ projectPath = project_path
30
+ File.join(Pathname.pwd,File.basename(projectPath, File.extname(projectPath)))
31
+ end
32
+
33
+ # 使用正则表达式匹配第一个字符前的空格数量
34
+ def self.count_spaces_before_first_character(str)
35
+ match = str.match(/\A\s*/)
36
+ match ? match[0].length : 0
37
+ end
38
+
39
+ # 使用字符串乘法添加指定数量的空格
40
+ def self.add_spaces_to_string(str, num_spaces)
41
+ spaces = ' ' * num_spaces
42
+ "#{spaces}#{str}"
43
+ end
44
+
45
+ def self.to_md5(string)
46
+ md5 = Digest::MD5.new
47
+ md5.update(string)
48
+ md5.hexdigest
49
+ end
50
+
51
+ def self.cache_privacy_fold
52
+ # 本地缓存目录
53
+ cache_directory = File.expand_path('~/.cache')
54
+
55
+ # 目标文件夹路径
56
+ target_directory = File.join(cache_directory, 'cocoapods-privacy', 'privacy')
57
+
58
+ # 如果文件夹不存在,则创建
59
+ FileUtils.mkdir_p(target_directory) unless Dir.exist?(target_directory)
60
+
61
+ target_directory
62
+ end
63
+
64
+ def self.cache_config_file
65
+ config_file = File.join(cache_privacy_fold, 'config.json')
66
+ end
67
+
68
+ # 创建默认隐私协议文件
69
+ def self.create_privacy_if_empty(file_path)
70
+ # 文件内容
71
+ file_content = <<~EOS
72
+ <?xml version="1.0" encoding="UTF-8"?>
73
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
74
+ <plist version="1.0">
75
+ <dict>
76
+ <key>NSPrivacyTracking</key>
77
+ <false/>
78
+ <key>NSPrivacyTrackingDomains</key>
79
+ <array/>
80
+ <key>NSPrivacyCollectedDataTypes</key>
81
+ <array/>
82
+ <key>NSPrivacyAccessedAPITypes</key>
83
+ <array/>
84
+ </dict>
85
+ </plist>
86
+ EOS
87
+
88
+ isCreate = create_file_and_fold_if_no_exit(file_path,file_content)
89
+ if isCreate
90
+ puts "【隐私清单】(初始化)存放地址 => #{file_path}"
91
+ end
92
+ end
93
+
94
+ # 创建文件,并写入默认值,文件路径不存在会自动创建
95
+ def self.create_file_and_fold_if_no_exit(file_path,file_content = nil)
96
+ folder_path = File.dirname(file_path)
97
+ FileUtils.mkdir_p(folder_path) unless File.directory?(folder_path)
98
+
99
+ # 创建文件(如果不存在/或为空)
100
+ if !File.exist?(file_path) || File.zero?(file_path)
101
+ File.open(file_path, 'w') do |file|
102
+ file.write(file_content)
103
+ end
104
+ return true
105
+ end
106
+ return false
107
+ end
108
+
109
+ # 查询group 中是否有执行路径的子group
110
+ def self.find_group_by_path(group,path)
111
+ result = nil
112
+ sub_group = group.children
113
+ if sub_group && !sub_group.empty?
114
+ sub_group.each do |item|
115
+ if item.path == path
116
+ result = item
117
+ break
118
+ end
119
+ end
120
+ end
121
+ result
122
+ end
123
+
124
+ end
@@ -0,0 +1,123 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'fileutils'
3
+ require 'cocoapods/podfile'
4
+ require 'cocoapods-privacy/command'
5
+
6
+ module Pod
7
+ # The Installer is responsible of taking a Podfile and transform it in the
8
+ # Pods libraries. It also integrates the user project so the Pods
9
+ # libraries can be used out of the box.
10
+ #
11
+ # The Installer is capable of doing incremental updates to an existing Pod
12
+ # installation.
13
+ #
14
+ # The Installer gets the information that it needs mainly from 3 files:
15
+ #
16
+ # - Podfile: The specification written by the user that contains
17
+ # information about targets and Pods.
18
+ # - Podfile.lock: Contains information about the pods that were previously
19
+ # installed and in concert with the Podfile provides information about
20
+ # which specific version of a Pod should be installed. This file is
21
+ # ignored in update mode.
22
+ # - Manifest.lock: A file contained in the Pods folder that keeps track of
23
+ # the pods installed in the local machine. This files is used once the
24
+ # exact versions of the Pods has been computed to detect if that version
25
+ # is already installed. This file is not intended to be kept under source
26
+ # control and is a copy of the Podfile.lock.
27
+ #
28
+ # The Installer is designed to work in environments where the Podfile folder
29
+ # is under source control and environments where it is not. The rest of the
30
+ # files, like the user project and the workspace are assumed to be under
31
+ # source control.
32
+ #
33
+ class Installer
34
+ autoload :Analyzer, 'cocoapods/installer/analyzer'
35
+ autoload :InstallationOptions, 'cocoapods/installer/installation_options'
36
+ autoload :PostInstallHooksContext, 'cocoapods/installer/post_install_hooks_context'
37
+ autoload :PreInstallHooksContext, 'cocoapods/installer/pre_install_hooks_context'
38
+ autoload :BaseInstallHooksContext, 'cocoapods/installer/base_install_hooks_context'
39
+ autoload :PostIntegrateHooksContext, 'cocoapods/installer/post_integrate_hooks_context'
40
+ autoload :PreIntegrateHooksContext, 'cocoapods/installer/pre_integrate_hooks_context'
41
+ autoload :SourceProviderHooksContext, 'cocoapods/installer/source_provider_hooks_context'
42
+ autoload :PodfileValidator, 'cocoapods/installer/podfile_validator'
43
+ autoload :PodSourceDownloader, 'cocoapods/installer/pod_source_downloader'
44
+ autoload :PodSourceInstaller, 'cocoapods/installer/pod_source_installer'
45
+ autoload :PodSourcePreparer, 'cocoapods/installer/pod_source_preparer'
46
+ autoload :UserProjectIntegrator, 'cocoapods/installer/user_project_integrator'
47
+ autoload :Xcode, 'cocoapods/installer/xcode'
48
+ autoload :SandboxHeaderPathsInstaller, 'cocoapods/installer/sandbox_header_paths_installer'
49
+ autoload :SandboxDirCleaner, 'cocoapods/installer/sandbox_dir_cleaner'
50
+ autoload :ProjectCache, 'cocoapods/installer/project_cache/project_cache'
51
+ autoload :TargetUUIDGenerator, 'cocoapods/installer/target_uuid_generator'
52
+
53
+
54
+ # 直接执行 pod privacy 时调用
55
+ def privacy_analysis(custom_folds)
56
+ prepare
57
+ resolve_dependencies
58
+ clean_sandbox
59
+
60
+ privacy_handle(custom_folds)
61
+ end
62
+
63
+ # hook pod install 命令
64
+ alias_method :privacy_origin_install!, :install!
65
+ def install!
66
+ privacy_origin_install!()
67
+
68
+ if !(Pod::Config.instance.is_privacy || !Pod::Config.instance.privacy_folds.empty?)
69
+ return
70
+ end
71
+
72
+ privacy_handle(Pod::Config.instance.privacy_folds)
73
+ end
74
+
75
+
76
+ def privacy_handle(custom_folds)
77
+ # 过滤出自身组件 && 自身没有隐私协议文件的spec
78
+ modules = @analysis_result.specifications.select {
79
+ |obj| obj.is_need_search_module && !obj.has_privacy
80
+ }
81
+
82
+ # 存储本地调试组件
83
+ development_folds = []
84
+
85
+ # 获取组件所在工程的pods 目录
86
+ pod_folds = modules.map{ |spec|
87
+ name = spec.name.split('/').first
88
+ fold = File.join(@sandbox.root,name)
89
+ if Dir.exist?(fold)
90
+ fold
91
+ else
92
+ development_pods = @sandbox.development_pods
93
+ if name && development_pods
94
+ podspec_file_path = development_pods[name]
95
+ if podspec_file_path && !podspec_file_path.empty?
96
+ podspec_fold_path = File.dirname(podspec_file_path)
97
+ source_files = spec.attributes_hash['source_files']
98
+ if source_files && !source_files.empty?
99
+ source_files.each do |file|
100
+ development_folds << File.join(podspec_fold_path,file)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ nil
106
+ end
107
+ }.compact
108
+
109
+
110
+ pod_folds += development_folds # 拼接本地调试和远端的pod目录
111
+ pod_folds += [PrivacyUtils.project_code_fold].compact # 拼接工程同名主目录
112
+ pod_folds += custom_folds || [] # 拼接外部传入的自定义目录
113
+ pod_folds = pod_folds.uniq # 去重
114
+
115
+ if pod_folds.empty?
116
+ puts "无组件或工程目录, 请检查工程"
117
+ else
118
+ # 处理工程隐私协议
119
+ PrivacyModule.load_project(pod_folds)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,53 @@
1
+
2
+ require 'cocoapods-core/specification/root_attribute_accessors'
3
+
4
+ module Pod
5
+ # The Specification provides a DSL to describe a Pod. A pod is defined as a
6
+ # library originating from a source. A specification can support detailed
7
+ # attributes for modules of code through subspecs.
8
+ #
9
+ # Usually it is stored in files with `podspec` extension.
10
+ #
11
+ class Specification
12
+
13
+ # 是否含有隐私协议文件
14
+ def has_privacy
15
+ resource_bundle = attributes_hash['resource_bundles']
16
+ resource_bundle && resource_bundle.to_s.include?('PrivacyInfo.xcprivacy')
17
+ end
18
+
19
+ # 是否为需要检索组件
20
+ def is_need_search_module
21
+ unless File.exist?(PrivacyUtils.cache_config_file)
22
+ raise Informative, "无配置文件,run `pod privacy config config_file` 进行配置"
23
+ end
24
+
25
+ #查找source(可能是subspec)
26
+ git_source = recursive_git_source(self)
27
+ unless git_source
28
+ return false
29
+ end
30
+
31
+ # 判断域名白名单 和 黑名单,确保该组件是自己的组件,第三方sdk不做检索
32
+ config = Privacy::Config.instance
33
+ git_source_whitelisted = config.source_white_list.any? { |item| git_source.include?(item) }
34
+ git_source_blacklisted = config.source_black_list.any? { |item| git_source.include?(item) }
35
+ git_source_whitelisted && !git_source_blacklisted
36
+ end
37
+
38
+ # 返回resource_bundles
39
+ def bb_resource_bundles
40
+ hash_value['resource_bundles']
41
+ end
42
+
43
+ private
44
+ def recursive_git_source(spec)
45
+ return nil unless spec
46
+ if spec.source && spec.source.key?(:git)
47
+ spec.source[:git]
48
+ else
49
+ recursive_git_source(spec.instance_variable_get(:@parent))
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1 @@
1
+ require 'cocoapods-privacy/gem_version'
@@ -0,0 +1 @@
1
+ require 'cocoapods-privacy/command'
@@ -0,0 +1,12 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ module Pod
4
+ describe Command::Privacy do
5
+ describe 'CLAide' do
6
+ it 'registers it self' do
7
+ Command.parse(%w{ privacy }).should.be.instance_of Command::Privacy
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,50 @@
1
+ require 'pathname'
2
+ ROOT = Pathname.new(File.expand_path('../../', __FILE__))
3
+ $:.unshift((ROOT + 'lib').to_s)
4
+ $:.unshift((ROOT + 'spec').to_s)
5
+
6
+ require 'bundler/setup'
7
+ require 'bacon'
8
+ require 'mocha-on-bacon'
9
+ require 'pretty_bacon'
10
+ require 'pathname'
11
+ require 'cocoapods'
12
+
13
+ Mocha::Configuration.prevent(:stubbing_non_existent_method)
14
+
15
+ require 'cocoapods_plugin'
16
+
17
+ #-----------------------------------------------------------------------------#
18
+
19
+ module Pod
20
+
21
+ # Disable the wrapping so the output is deterministic in the tests.
22
+ #
23
+ UI.disable_wrap = true
24
+
25
+ # Redirects the messages to an internal store.
26
+ #
27
+ module UI
28
+ @output = ''
29
+ @warnings = ''
30
+
31
+ class << self
32
+ attr_accessor :output
33
+ attr_accessor :warnings
34
+
35
+ def puts(message = '')
36
+ @output << "#{message}\n"
37
+ end
38
+
39
+ def warn(message = '', actions = [])
40
+ @warnings << "#{message}\n"
41
+ end
42
+
43
+ def print(message)
44
+ @output << message
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ #-----------------------------------------------------------------------------#
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cocoapods-privacy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - youhui
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A short description of cocoapods-privacy.
42
+ email:
43
+ - developer_yh@163.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.txt
49
+ - README.md
50
+ - lib/cocoapods-privacy.rb
51
+ - lib/cocoapods-privacy/command.rb
52
+ - lib/cocoapods-privacy/command/install.rb
53
+ - lib/cocoapods-privacy/command/privacy.rb
54
+ - lib/cocoapods-privacy/command/privacy/config.rb
55
+ - lib/cocoapods-privacy/command/privacy/install.rb
56
+ - lib/cocoapods-privacy/command/privacy/spec.rb
57
+ - lib/cocoapods-privacy/gem_version.rb
58
+ - lib/cocoapods-privacy/privacy/PrivacyConfig.rb
59
+ - lib/cocoapods-privacy/privacy/PrivacyHunter.rb
60
+ - lib/cocoapods-privacy/privacy/PrivacyModule.rb
61
+ - lib/cocoapods-privacy/privacy/PrivacyUtils.rb
62
+ - lib/cocoapods-privacy/privacy/privacy_installer_hook.rb
63
+ - lib/cocoapods-privacy/privacy/privacy_specification_hook.rb
64
+ - lib/cocoapods_plugin.rb
65
+ - spec/command/privacy_spec.rb
66
+ - spec/spec_helper.rb
67
+ homepage: https://github.com/ymoyao/cocoapods-privacy
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.4.21
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: A longer description of cocoapods-privacy.
90
+ test_files:
91
+ - spec/command/privacy_spec.rb
92
+ - spec/spec_helper.rb