app-info 2.2.0
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +40 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +96 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +246 -0
- data/Rakefile +10 -0
- data/app_info.gemspec +33 -0
- data/lib/app-info.rb +3 -0
- data/lib/app_info.rb +88 -0
- data/lib/app_info/apk.rb +165 -0
- data/lib/app_info/core_ext/object/try.rb +114 -0
- data/lib/app_info/dsym.rb +150 -0
- data/lib/app_info/ipa.rb +173 -0
- data/lib/app_info/ipa/info_plist.rb +146 -0
- data/lib/app_info/ipa/mobile_provision.rb +260 -0
- data/lib/app_info/proguard.rb +90 -0
- data/lib/app_info/util.rb +58 -0
- data/lib/app_info/version.rb +5 -0
- metadata +231 -0
data/lib/app_info/ipa.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'macho'
|
4
|
+
require 'pngdefry'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'forwardable'
|
7
|
+
require 'cfpropertylist'
|
8
|
+
require 'app_info/util'
|
9
|
+
|
10
|
+
module AppInfo
|
11
|
+
# IPA parser
|
12
|
+
class IPA
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
attr_reader :file
|
16
|
+
|
17
|
+
# iOS Export types
|
18
|
+
module ExportType
|
19
|
+
DEBUG = 'Debug'
|
20
|
+
ADHOC = 'AdHoc'
|
21
|
+
INHOUSE = 'inHouse'
|
22
|
+
RELEASE = 'Release'
|
23
|
+
UNKOWN = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(file)
|
27
|
+
@file = file
|
28
|
+
end
|
29
|
+
|
30
|
+
def size(humanable = false)
|
31
|
+
AppInfo::Util.file_size(@file, humanable)
|
32
|
+
end
|
33
|
+
|
34
|
+
def os
|
35
|
+
AppInfo::Platform::IOS
|
36
|
+
end
|
37
|
+
alias file_type os
|
38
|
+
|
39
|
+
def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
|
40
|
+
:release_version, :identifier, :bundle_id, :display_name,
|
41
|
+
:bundle_name, :icons, :min_sdk_version, :device_type
|
42
|
+
|
43
|
+
def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
|
44
|
+
:profile_name, :expired_date
|
45
|
+
|
46
|
+
def distribution_name
|
47
|
+
"#{profile_name} - #{team_name}" if profile_name && team_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def release_type
|
51
|
+
if stored?
|
52
|
+
ExportType::RELEASE
|
53
|
+
else
|
54
|
+
build_type
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_type
|
59
|
+
if mobileprovision?
|
60
|
+
if devices
|
61
|
+
ExportType::ADHOC
|
62
|
+
else
|
63
|
+
ExportType::INHOUSE
|
64
|
+
end
|
65
|
+
else
|
66
|
+
ExportType::DEBUG
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def archs
|
71
|
+
return unless File.exist?(bundle_path)
|
72
|
+
|
73
|
+
file = MachO.open(bundle_path)
|
74
|
+
case file
|
75
|
+
when MachO::MachOFile
|
76
|
+
[file.cpusubtype]
|
77
|
+
else
|
78
|
+
file.machos.each_with_object([]) do |arch, obj|
|
79
|
+
obj << arch.cpusubtype
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
alias architectures archs
|
84
|
+
|
85
|
+
def stored?
|
86
|
+
metadata? ? true : false
|
87
|
+
end
|
88
|
+
|
89
|
+
def hide_developer_certificates
|
90
|
+
mobileprovision.delete('DeveloperCertificates') if mobileprovision?
|
91
|
+
end
|
92
|
+
|
93
|
+
def mobileprovision
|
94
|
+
return unless mobileprovision?
|
95
|
+
return @mobileprovision if @mobileprovision
|
96
|
+
|
97
|
+
@mobileprovision = MobileProvision.new(mobileprovision_path)
|
98
|
+
end
|
99
|
+
|
100
|
+
def mobileprovision?
|
101
|
+
File.exist?mobileprovision_path
|
102
|
+
end
|
103
|
+
|
104
|
+
def mobileprovision_path
|
105
|
+
filename = 'embedded.mobileprovision'
|
106
|
+
@mobileprovision_path ||= File.join(@file, filename)
|
107
|
+
unless File.exist?(@mobileprovision_path)
|
108
|
+
@mobileprovision_path = File.join(app_path, filename)
|
109
|
+
end
|
110
|
+
|
111
|
+
@mobileprovision_path
|
112
|
+
end
|
113
|
+
|
114
|
+
def metadata
|
115
|
+
return unless metadata?
|
116
|
+
|
117
|
+
@metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
|
118
|
+
end
|
119
|
+
|
120
|
+
def metadata?
|
121
|
+
File.exist?(metadata_path)
|
122
|
+
end
|
123
|
+
|
124
|
+
def metadata_path
|
125
|
+
@metadata_path ||= File.join(contents, 'iTunesMetadata.plist')
|
126
|
+
end
|
127
|
+
|
128
|
+
def bundle_path
|
129
|
+
@bundle_path ||= File.join(app_path, info.bundle_name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def info
|
133
|
+
@info ||= InfoPlist.new(app_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def app_path
|
137
|
+
@app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
|
138
|
+
end
|
139
|
+
|
140
|
+
def cleanup!
|
141
|
+
return unless @contents
|
142
|
+
|
143
|
+
FileUtils.rm_rf(@contents)
|
144
|
+
|
145
|
+
@contents = nil
|
146
|
+
@icons = nil
|
147
|
+
@app_path = nil
|
148
|
+
@metadata = nil
|
149
|
+
@metadata_path = nil
|
150
|
+
@info = nil
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def contents
|
156
|
+
@contents ||= Util.unarchive(@file, path: 'ios')
|
157
|
+
end
|
158
|
+
|
159
|
+
def icons_root_path
|
160
|
+
iphone = 'CFBundleIcons'
|
161
|
+
ipad = 'CFBundleIcons~ipad'
|
162
|
+
|
163
|
+
case device_type
|
164
|
+
when 'iPhone'
|
165
|
+
[iphone]
|
166
|
+
when 'iPad'
|
167
|
+
[ipad]
|
168
|
+
when 'Universal'
|
169
|
+
[iphone, ipad]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cfpropertylist'
|
4
|
+
require 'app_info/util'
|
5
|
+
|
6
|
+
module AppInfo
|
7
|
+
# iOS Info.plist parser
|
8
|
+
class InfoPlist
|
9
|
+
def initialize(app_path)
|
10
|
+
@app_path = app_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_version
|
14
|
+
info.try(:[], 'CFBundleVersion')
|
15
|
+
end
|
16
|
+
|
17
|
+
def release_version
|
18
|
+
info.try(:[], 'CFBundleShortVersionString')
|
19
|
+
end
|
20
|
+
|
21
|
+
def identifier
|
22
|
+
info.try(:[], 'CFBundleIdentifier')
|
23
|
+
end
|
24
|
+
alias bundle_id identifier
|
25
|
+
|
26
|
+
def name
|
27
|
+
display_name || bundle_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def display_name
|
31
|
+
info.try(:[], 'CFBundleDisplayName')
|
32
|
+
end
|
33
|
+
|
34
|
+
def bundle_name
|
35
|
+
info.try(:[], 'CFBundleName')
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Extract the Minimum OS Version from the Info.plist
|
40
|
+
#
|
41
|
+
def min_sdk_version
|
42
|
+
info.try(:[], 'MinimumOSVersion')
|
43
|
+
end
|
44
|
+
|
45
|
+
def icons
|
46
|
+
return @icons if @icons
|
47
|
+
|
48
|
+
@icons = []
|
49
|
+
icons_root_path.each do |name|
|
50
|
+
icon_array = info.try(:[], name)
|
51
|
+
.try(:[], 'CFBundlePrimaryIcon')
|
52
|
+
.try(:[], 'CFBundleIconFiles')
|
53
|
+
|
54
|
+
next if icon_array.nil? || icon_array.empty?
|
55
|
+
|
56
|
+
icon_array.each do |items|
|
57
|
+
Dir.glob(File.join(@app_path, "#{items}*")).find_all.each do |file|
|
58
|
+
dict = {
|
59
|
+
name: File.basename(file),
|
60
|
+
file: file,
|
61
|
+
dimensions: Pngdefry.dimensions(file)
|
62
|
+
}
|
63
|
+
|
64
|
+
@icons.push(dict)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
@icons
|
70
|
+
end
|
71
|
+
|
72
|
+
def device_type
|
73
|
+
device_family = info.try(:[], 'UIDeviceFamily')
|
74
|
+
if device_family.length == 1
|
75
|
+
case device_family
|
76
|
+
when [1]
|
77
|
+
'iPhone'
|
78
|
+
when [2]
|
79
|
+
'iPad'
|
80
|
+
end
|
81
|
+
elsif device_family.length == 2 && device_family == [1, 2]
|
82
|
+
'Universal'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def iphone?
|
87
|
+
device_type == 'iPhone'
|
88
|
+
end
|
89
|
+
|
90
|
+
def ipad?
|
91
|
+
device_type == 'iPad'
|
92
|
+
end
|
93
|
+
|
94
|
+
def universal?
|
95
|
+
device_type == 'Universal'
|
96
|
+
end
|
97
|
+
|
98
|
+
def release_type
|
99
|
+
if stored?
|
100
|
+
'Store'
|
101
|
+
else
|
102
|
+
build_type
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def [](key)
|
107
|
+
info.try(:[], key.to_s)
|
108
|
+
end
|
109
|
+
|
110
|
+
def method_missing(method_name, *args, &block)
|
111
|
+
info.try(:[], Util.format_key(method_name)) ||
|
112
|
+
info.send(method_name) ||
|
113
|
+
super
|
114
|
+
end
|
115
|
+
|
116
|
+
def respond_to_missing?(method_name, *args)
|
117
|
+
info.key?(Util.format_key(method_name)) ||
|
118
|
+
info.respond_to?(method_name) ||
|
119
|
+
super
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def info
|
125
|
+
@info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
|
126
|
+
end
|
127
|
+
|
128
|
+
def info_path
|
129
|
+
File.join(@app_path, 'Info.plist')
|
130
|
+
end
|
131
|
+
|
132
|
+
IPHONE_KEY = 'CFBundleIcons'
|
133
|
+
IPAD_KEY = 'CFBundleIcons~ipad'
|
134
|
+
|
135
|
+
def icons_root_path
|
136
|
+
case device_type
|
137
|
+
when 'iPhone'
|
138
|
+
[IPHONE_KEY]
|
139
|
+
when 'iPad'
|
140
|
+
[IPAD_KEY]
|
141
|
+
when 'Universal'
|
142
|
+
[IPHONE_KEY, IPAD_KEY]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'cfpropertylist'
|
5
|
+
require 'app_info/util'
|
6
|
+
|
7
|
+
module AppInfo
|
8
|
+
# .mobileprovision file parser
|
9
|
+
class MobileProvision
|
10
|
+
def initialize(path)
|
11
|
+
@path = path
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
mobileprovision.try(:[], 'Name')
|
16
|
+
end
|
17
|
+
|
18
|
+
def app_name
|
19
|
+
mobileprovision.try(:[], 'AppIDName')
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
return :development if development?
|
24
|
+
return :adhoc if adhoc?
|
25
|
+
return :appstore if appstore?
|
26
|
+
return :inhouse if inhouse?
|
27
|
+
end
|
28
|
+
|
29
|
+
def platforms
|
30
|
+
return unless platforms = mobileprovision.try(:[], 'Platform')
|
31
|
+
|
32
|
+
platforms.map { |v|
|
33
|
+
v = 'macOS' if v == 'OSX'
|
34
|
+
v.downcase.to_sym
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def platform
|
39
|
+
platforms[0]
|
40
|
+
end
|
41
|
+
|
42
|
+
def devices
|
43
|
+
mobileprovision.try(:[], 'ProvisionedDevices')
|
44
|
+
end
|
45
|
+
|
46
|
+
def team_identifier
|
47
|
+
mobileprovision.try(:[], 'TeamIdentifier')
|
48
|
+
end
|
49
|
+
|
50
|
+
def team_name
|
51
|
+
mobileprovision.try(:[], 'TeamName')
|
52
|
+
end
|
53
|
+
|
54
|
+
def profile_name
|
55
|
+
mobileprovision.try(:[], 'Name')
|
56
|
+
end
|
57
|
+
|
58
|
+
def created_date
|
59
|
+
mobileprovision.try(:[], 'CreationDate')
|
60
|
+
end
|
61
|
+
|
62
|
+
def expired_date
|
63
|
+
mobileprovision.try(:[], 'ExpirationDate')
|
64
|
+
end
|
65
|
+
|
66
|
+
def entitlements
|
67
|
+
mobileprovision.try(:[], 'Entitlements')
|
68
|
+
end
|
69
|
+
|
70
|
+
def developer_certs
|
71
|
+
certs = mobileprovision.try(:[], 'DeveloperCertificates')
|
72
|
+
return if certs.empty?
|
73
|
+
|
74
|
+
certs.each_with_object([]) do |cert, obj|
|
75
|
+
obj << DeveloperCertificate.new(cert)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Detect is development type of mobileprovision
|
80
|
+
#
|
81
|
+
# related link: https://stackoverflow.com/questions/1003066/what-does-get-task-allow-do-in-xcode
|
82
|
+
def development?
|
83
|
+
case platform.downcase.to_sym
|
84
|
+
when :ios
|
85
|
+
entitlements['get-task-allow'] == true
|
86
|
+
when :macos
|
87
|
+
!devices.nil?
|
88
|
+
else
|
89
|
+
raise Error, "Not implement with platform: #{platform}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Detect app store type
|
94
|
+
#
|
95
|
+
# related link: https://developer.apple.com/library/archive/qa/qa1830/_index.html
|
96
|
+
def appstore?
|
97
|
+
case platform.downcase.to_sym
|
98
|
+
when :ios
|
99
|
+
!development? && entitlements.key?('beta-reports-active')
|
100
|
+
when :macos
|
101
|
+
!development?
|
102
|
+
else
|
103
|
+
raise Error, "Not implement with platform: #{platform}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def adhoc?
|
108
|
+
return false if platform == :macos # macOS no need adhoc
|
109
|
+
|
110
|
+
!development? && !devices.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
def inhouse?
|
114
|
+
return false if platform == :macos # macOS no need adhoc
|
115
|
+
|
116
|
+
!development? && !adhoc? && !appstore?
|
117
|
+
end
|
118
|
+
|
119
|
+
# Enabled Capabilites
|
120
|
+
#
|
121
|
+
# Related link: https://developer.apple.com/support/app-capabilities/
|
122
|
+
def enabled_capabilities
|
123
|
+
capabilities = []
|
124
|
+
if adhoc? || appstore?
|
125
|
+
capabilities << 'In-App Purchase' << 'GameKit'
|
126
|
+
end
|
127
|
+
|
128
|
+
entitlements.each do |key, value|
|
129
|
+
case key
|
130
|
+
when 'aps-environment'
|
131
|
+
capabilities << 'Push Notifications'
|
132
|
+
when 'com.apple.developer.applesignin'
|
133
|
+
capabilities << 'Sign In with Apple'
|
134
|
+
when 'com.apple.developer.siri'
|
135
|
+
capabilities << 'SiriKit'
|
136
|
+
when 'com.apple.security.application-groups'
|
137
|
+
capabilities << 'App Groups'
|
138
|
+
when 'com.apple.developer.associated-domains'
|
139
|
+
capabilities << 'Associated Domains'
|
140
|
+
when 'com.apple.developer.default-data-protection'
|
141
|
+
capabilities << 'Data Protection'
|
142
|
+
when 'com.apple.developer.networking.networkextension'
|
143
|
+
capabilities << 'Network Extensions'
|
144
|
+
when 'com.apple.developer.networking.vpn.api'
|
145
|
+
capabilities << 'Personal VPN'
|
146
|
+
when 'com.apple.developer.healthkit', 'com.apple.developer.healthkit.access'
|
147
|
+
capabilities << 'HealthKit' unless capabilities.include?('HealthKit')
|
148
|
+
when 'com.apple.developer.icloud-services', 'com.apple.developer.icloud-container-identifiers'
|
149
|
+
capabilities << 'iCloud' unless capabilities.include?('iCloud')
|
150
|
+
when 'com.apple.developer.in-app-payments'
|
151
|
+
capabilities << 'Apple Pay'
|
152
|
+
when 'com.apple.developer.homekit'
|
153
|
+
capabilities << 'HomeKit'
|
154
|
+
when 'com.apple.developer.user-fonts'
|
155
|
+
capabilities << 'Fonts'
|
156
|
+
when 'com.apple.developer.pass-type-identifiers'
|
157
|
+
capabilities << 'Wallet'
|
158
|
+
when 'inter-app-audio'
|
159
|
+
capabilities << 'Inter-App Audio'
|
160
|
+
when 'com.apple.developer.networking.multipath'
|
161
|
+
capabilities << 'Multipath'
|
162
|
+
when 'com.apple.developer.authentication-services.autofill-credential-provider'
|
163
|
+
capabilities << 'AutoFill Credential Provider'
|
164
|
+
when 'com.apple.developer.networking.wifi-info'
|
165
|
+
capabilities << 'Access WiFi Information'
|
166
|
+
when 'com.apple.external-accessory.wireless-configuration'
|
167
|
+
capabilities << 'Wireless Accessory Configuration'
|
168
|
+
when 'com.apple.developer.kernel.extended-virtual-addressing'
|
169
|
+
capabilities << 'Extended Virtual Address Space'
|
170
|
+
when 'com.apple.developer.nfc.readersession.formats'
|
171
|
+
capabilities << 'NFC Tag Reading'
|
172
|
+
when 'com.apple.developer.ClassKit-environment'
|
173
|
+
capabilities << 'ClassKit'
|
174
|
+
when 'com.apple.developer.networking.HotspotConfiguration'
|
175
|
+
capabilities << 'Hotspot'
|
176
|
+
when 'com.apple.developer.devicecheck.appattest-environment'
|
177
|
+
capabilities << 'App Attest'
|
178
|
+
when 'com.apple.developer.coremedia.hls.low-latency'
|
179
|
+
capabilities << 'Low Latency HLS'
|
180
|
+
when 'com.apple.developer.associated-domains.mdm-managed'
|
181
|
+
capabilities << 'MDM Managed Associated Domains'
|
182
|
+
# macOS Only
|
183
|
+
when 'com.apple.developer.maps'
|
184
|
+
capabilities << 'Maps'
|
185
|
+
when 'com.apple.developer.system-extension.install'
|
186
|
+
capabilities << 'System Extension'
|
187
|
+
when 'com.apple.developer.networking.custom-protocol'
|
188
|
+
capabilities << 'Custom Network Protocol'
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
capabilities
|
193
|
+
end
|
194
|
+
|
195
|
+
def [](key)
|
196
|
+
mobileprovision.try(:[], key.to_s)
|
197
|
+
end
|
198
|
+
|
199
|
+
def empty?
|
200
|
+
mobileprovision.nil?
|
201
|
+
end
|
202
|
+
|
203
|
+
def mobileprovision
|
204
|
+
return @mobileprovision = nil unless File.exist?(@path)
|
205
|
+
|
206
|
+
data = File.read(@path)
|
207
|
+
data = strip_plist_wrapper(data) unless bplist?(data)
|
208
|
+
list = CFPropertyList::List.new(data: data).value
|
209
|
+
@mobileprovision = CFPropertyList.native_types(list)
|
210
|
+
rescue CFFormatError
|
211
|
+
@mobileprovision = nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def method_missing(method_name, *args, &block)
|
215
|
+
mobileprovision.try(:[], Util.format_key(method_name)) ||
|
216
|
+
mobileprovision.send(method_name) ||
|
217
|
+
super
|
218
|
+
end
|
219
|
+
|
220
|
+
def respond_to_missing?(method_name, *args)
|
221
|
+
mobileprovision.key?(Util.format_key(method_name)) ||
|
222
|
+
mobileprovision.respond_to?(method_name) ||
|
223
|
+
super
|
224
|
+
end
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
def bplist?(raw)
|
229
|
+
raw[0..5] == 'bplist'
|
230
|
+
end
|
231
|
+
|
232
|
+
def strip_plist_wrapper(raw)
|
233
|
+
end_tag = '</plist>'
|
234
|
+
start_point = raw.index('<?xml version=')
|
235
|
+
end_point = raw.index(end_tag) + end_tag.size - 1
|
236
|
+
raw[start_point..end_point]
|
237
|
+
end
|
238
|
+
|
239
|
+
# Developer Certificate
|
240
|
+
class DeveloperCertificate
|
241
|
+
attr_reader :raw
|
242
|
+
|
243
|
+
def initialize(data)
|
244
|
+
@raw = OpenSSL::X509::Certificate.new(data)
|
245
|
+
end
|
246
|
+
|
247
|
+
def name
|
248
|
+
@raw.subject.to_a.find { |name, _, _| name == 'CN' }[1]
|
249
|
+
end
|
250
|
+
|
251
|
+
def created_date
|
252
|
+
@raw.not_after
|
253
|
+
end
|
254
|
+
|
255
|
+
def expired_date
|
256
|
+
@raw.not_before
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|