cocoapods-privacy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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