tusktsk 2.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 +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +14 -0
- data/README.md +759 -0
- data/cli/main.rb +1488 -0
- data/exe/tsk +10 -0
- data/lib/peanut_config.rb +621 -0
- data/lib/tusk/license.rb +303 -0
- data/lib/tusk/protection.rb +180 -0
- data/lib/tusk_lang/shell_storage.rb +104 -0
- data/lib/tusk_lang/tsk.rb +501 -0
- data/lib/tusk_lang/tsk_parser.rb +234 -0
- data/lib/tusk_lang/tsk_parser_enhanced.rb +563 -0
- data/lib/tusk_lang/version.rb +5 -0
- data/lib/tusk_lang.rb +14 -0
- metadata +249 -0
data/lib/tusk/license.rb
ADDED
@@ -0,0 +1,303 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'json'
|
5
|
+
require 'net/http'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'time'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'yaml'
|
10
|
+
|
11
|
+
module Tusk
|
12
|
+
# TuskLang SDK License Validation Module
|
13
|
+
# Enterprise-grade license validation for Ruby SDK
|
14
|
+
class License
|
15
|
+
attr_reader :license_key, :api_key, :session_id
|
16
|
+
|
17
|
+
def initialize(license_key, api_key, cache_dir = nil)
|
18
|
+
@license_key = license_key
|
19
|
+
@api_key = api_key
|
20
|
+
@session_id = SecureRandom.uuid
|
21
|
+
@license_cache = {}
|
22
|
+
@validation_history = []
|
23
|
+
@expiration_warnings = []
|
24
|
+
|
25
|
+
# Set up offline cache directory
|
26
|
+
@cache_dir = cache_dir || File.join(Dir.home, '.tusk', 'license_cache')
|
27
|
+
FileUtils.mkdir_p(@cache_dir)
|
28
|
+
@cache_file = File.join(@cache_dir, Digest::MD5.hexdigest(license_key) + '.cache')
|
29
|
+
|
30
|
+
# Load offline cache if exists
|
31
|
+
load_offline_cache
|
32
|
+
|
33
|
+
# Set up logger
|
34
|
+
@logger = Logger.new(STDERR)
|
35
|
+
@logger.progname = 'TuskLicense'
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_license_key
|
39
|
+
return { valid: false, error: 'Invalid license key format' } if @license_key.nil? || @license_key.length < 32
|
40
|
+
return { valid: false, error: 'Invalid license key prefix' } unless @license_key.start_with?('TUSK-')
|
41
|
+
|
42
|
+
checksum = Digest::SHA256.hexdigest(@license_key)
|
43
|
+
return { valid: false, error: 'Invalid license key checksum' } unless checksum.start_with?('tusk')
|
44
|
+
|
45
|
+
{ valid: true, checksum: checksum }
|
46
|
+
rescue StandardError => e
|
47
|
+
{ valid: false, error: e.message }
|
48
|
+
end
|
49
|
+
|
50
|
+
def verify_license_server(server_url = 'https://api.tusklang.org/v1/license')
|
51
|
+
uri = URI(server_url)
|
52
|
+
|
53
|
+
data = {
|
54
|
+
license_key: @license_key,
|
55
|
+
session_id: @session_id,
|
56
|
+
timestamp: Time.now.to_i
|
57
|
+
}
|
58
|
+
|
59
|
+
# Generate signature
|
60
|
+
signature = OpenSSL::HMAC.hexdigest('SHA256', @api_key, data.sort.to_h.to_json)
|
61
|
+
data[:signature] = signature
|
62
|
+
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
64
|
+
http.use_ssl = true if uri.scheme == 'https'
|
65
|
+
http.open_timeout = 10
|
66
|
+
http.read_timeout = 10
|
67
|
+
|
68
|
+
request = Net::HTTP::Post.new(uri.path)
|
69
|
+
request['Authorization'] = "Bearer #{@api_key}"
|
70
|
+
request['Content-Type'] = 'application/json'
|
71
|
+
request['User-Agent'] = 'TuskLang-Ruby-SDK/1.0.0'
|
72
|
+
request.body = data.to_json
|
73
|
+
|
74
|
+
response = http.request(request)
|
75
|
+
|
76
|
+
if response.code == '200'
|
77
|
+
result = JSON.parse(response.body)
|
78
|
+
|
79
|
+
# Update in-memory cache
|
80
|
+
@license_cache[@license_key] = {
|
81
|
+
'data' => result,
|
82
|
+
'timestamp' => Time.now.to_i,
|
83
|
+
'expires' => Time.now.to_i + 3600 # Cache for 1 hour
|
84
|
+
}
|
85
|
+
|
86
|
+
# Update offline cache
|
87
|
+
save_offline_cache(result)
|
88
|
+
|
89
|
+
result
|
90
|
+
else
|
91
|
+
@logger.warn("Server returned error: #{response.code}")
|
92
|
+
fallback_to_offline_cache("Server error: #{response.code}")
|
93
|
+
end
|
94
|
+
|
95
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
96
|
+
@logger.warn("Network timeout during license validation: #{e.message}")
|
97
|
+
fallback_to_offline_cache("Network timeout: #{e.message}")
|
98
|
+
rescue StandardError => e
|
99
|
+
@logger.error("Unexpected error during license validation: #{e.message}")
|
100
|
+
fallback_to_offline_cache(e.message)
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_license_expiration
|
104
|
+
parts = @license_key.split('-')
|
105
|
+
return { expired: true, error: 'Invalid license key format' } if parts.length < 4
|
106
|
+
|
107
|
+
expiration_str = parts.last
|
108
|
+
expiration_timestamp = expiration_str.to_i(16)
|
109
|
+
expiration_date = Time.at(expiration_timestamp)
|
110
|
+
current_date = Time.now
|
111
|
+
|
112
|
+
if expiration_date < current_date
|
113
|
+
days_overdue = ((current_date - expiration_date) / 86400).to_i
|
114
|
+
return {
|
115
|
+
expired: true,
|
116
|
+
expiration_date: expiration_date.iso8601,
|
117
|
+
days_overdue: days_overdue
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
days_remaining = ((expiration_date - current_date) / 86400).to_i
|
122
|
+
|
123
|
+
if days_remaining <= 30
|
124
|
+
@expiration_warnings << {
|
125
|
+
timestamp: Time.now.to_i,
|
126
|
+
days_remaining: days_remaining
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
{
|
131
|
+
expired: false,
|
132
|
+
expiration_date: expiration_date.iso8601,
|
133
|
+
days_remaining: days_remaining,
|
134
|
+
warning: days_remaining <= 30
|
135
|
+
}
|
136
|
+
|
137
|
+
rescue StandardError => e
|
138
|
+
{ expired: true, error: e.message }
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_license_permissions(feature)
|
142
|
+
# Check cached license data
|
143
|
+
if @license_cache.key?(@license_key)
|
144
|
+
cache_data = @license_cache[@license_key]
|
145
|
+
if Time.now.to_i < cache_data['expires']
|
146
|
+
license_data = cache_data['data']
|
147
|
+
if license_data['features']&.is_a?(Array)
|
148
|
+
if license_data['features'].include?(feature)
|
149
|
+
return { allowed: true, feature: feature }
|
150
|
+
else
|
151
|
+
return { allowed: false, feature: feature, error: 'Feature not licensed' }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Fallback to basic validation
|
158
|
+
case feature
|
159
|
+
when 'basic', 'core', 'standard'
|
160
|
+
{ allowed: true, feature: feature }
|
161
|
+
when 'premium', 'enterprise'
|
162
|
+
if @license_key.upcase.include?('PREMIUM') || @license_key.upcase.include?('ENTERPRISE')
|
163
|
+
{ allowed: true, feature: feature }
|
164
|
+
else
|
165
|
+
{ allowed: false, feature: feature, error: 'Premium license required' }
|
166
|
+
end
|
167
|
+
else
|
168
|
+
{ allowed: false, feature: feature, error: 'Unknown feature' }
|
169
|
+
end
|
170
|
+
|
171
|
+
rescue StandardError => e
|
172
|
+
{ allowed: false, feature: feature, error: e.message }
|
173
|
+
end
|
174
|
+
|
175
|
+
def license_info
|
176
|
+
validation_result = validate_license_key
|
177
|
+
expiration_result = check_license_expiration
|
178
|
+
|
179
|
+
info = {
|
180
|
+
license_key: "#{@license_key[0..7]}...#{@license_key[-4..-1]}",
|
181
|
+
session_id: @session_id,
|
182
|
+
validation: validation_result,
|
183
|
+
expiration: expiration_result,
|
184
|
+
cache_status: @license_cache.key?(@license_key) ? 'cached' : 'not_cached',
|
185
|
+
validation_count: @validation_history.length,
|
186
|
+
warnings: @expiration_warnings.length
|
187
|
+
}
|
188
|
+
|
189
|
+
if @license_cache.key?(@license_key)
|
190
|
+
cache_data = @license_cache[@license_key]
|
191
|
+
info[:cached_data] = cache_data['data']
|
192
|
+
info[:cache_age] = Time.now.to_i - cache_data['timestamp']
|
193
|
+
end
|
194
|
+
|
195
|
+
info
|
196
|
+
|
197
|
+
rescue StandardError => e
|
198
|
+
{ error: e.message }
|
199
|
+
end
|
200
|
+
|
201
|
+
def refresh_license_cache
|
202
|
+
@license_cache.delete(@license_key)
|
203
|
+
verify_license_server
|
204
|
+
end
|
205
|
+
|
206
|
+
def log_validation_attempt(success, details = '')
|
207
|
+
@validation_history << {
|
208
|
+
timestamp: Time.now.to_i,
|
209
|
+
success: success,
|
210
|
+
details: details,
|
211
|
+
session_id: @session_id
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
def validation_history
|
216
|
+
@validation_history
|
217
|
+
end
|
218
|
+
|
219
|
+
def clear_validation_history
|
220
|
+
@validation_history.clear
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def load_offline_cache
|
226
|
+
return unless File.exist?(@cache_file)
|
227
|
+
|
228
|
+
cached_data = YAML.load_file(@cache_file)
|
229
|
+
|
230
|
+
# Verify the cache is for the correct license key
|
231
|
+
key_hash = Digest::SHA256.hexdigest(@license_key)
|
232
|
+
if cached_data['license_key_hash'] == key_hash
|
233
|
+
@offline_cache = cached_data
|
234
|
+
@logger.info('Loaded offline license cache')
|
235
|
+
else
|
236
|
+
@offline_cache = nil
|
237
|
+
@logger.warn('Offline cache key mismatch')
|
238
|
+
end
|
239
|
+
rescue StandardError => e
|
240
|
+
@logger.error("Failed to load offline cache: #{e.message}")
|
241
|
+
@offline_cache = nil
|
242
|
+
end
|
243
|
+
|
244
|
+
def save_offline_cache(license_data)
|
245
|
+
cache_data = {
|
246
|
+
'license_key_hash' => Digest::SHA256.hexdigest(@license_key),
|
247
|
+
'license_data' => license_data,
|
248
|
+
'timestamp' => Time.now.to_i,
|
249
|
+
'expiration' => check_license_expiration
|
250
|
+
}
|
251
|
+
|
252
|
+
File.write(@cache_file, cache_data.to_yaml)
|
253
|
+
@offline_cache = cache_data
|
254
|
+
@logger.info('Saved license data to offline cache')
|
255
|
+
rescue StandardError => e
|
256
|
+
@logger.error("Failed to save offline cache: #{e.message}")
|
257
|
+
end
|
258
|
+
|
259
|
+
def fallback_to_offline_cache(error_msg)
|
260
|
+
if @offline_cache && @offline_cache['license_data']
|
261
|
+
cache_age = Time.now.to_i - @offline_cache['timestamp']
|
262
|
+
cache_age_days = cache_age.to_f / 86400
|
263
|
+
|
264
|
+
# Check if cached license is not expired
|
265
|
+
expiration = @offline_cache['expiration'] || {}
|
266
|
+
unless expiration['expired']
|
267
|
+
@logger.warn("Using offline license cache (age: #{'%.1f' % cache_age_days} days)")
|
268
|
+
return @offline_cache['license_data'].merge(
|
269
|
+
'offline_mode' => true,
|
270
|
+
'cache_age_days' => cache_age_days,
|
271
|
+
'warning' => "Operating in offline mode due to: #{error_msg}"
|
272
|
+
)
|
273
|
+
else
|
274
|
+
return {
|
275
|
+
'valid' => false,
|
276
|
+
'error' => "License expired and server unreachable: #{error_msg}",
|
277
|
+
'offline_cache_expired' => true
|
278
|
+
}
|
279
|
+
end
|
280
|
+
else
|
281
|
+
{
|
282
|
+
'valid' => false,
|
283
|
+
'error' => "No offline cache available: #{error_msg}",
|
284
|
+
'offline_cache_missing' => true
|
285
|
+
}
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Global license instance
|
291
|
+
@license_instance = nil
|
292
|
+
|
293
|
+
class << self
|
294
|
+
def initialize_license(license_key, api_key, cache_dir = nil)
|
295
|
+
@license_instance = License.new(license_key, api_key, cache_dir)
|
296
|
+
end
|
297
|
+
|
298
|
+
def license
|
299
|
+
raise 'License not initialized. Call Tusk.initialize_license() first.' unless @license_instance
|
300
|
+
@license_instance
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
require 'json'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
module Tusk
|
9
|
+
# TuskLang SDK Protection Core Module
|
10
|
+
# Enterprise-grade protection for Ruby SDK
|
11
|
+
class Protection
|
12
|
+
attr_reader :license_key, :api_key, :session_id, :integrity_checks, :usage_metrics
|
13
|
+
|
14
|
+
def initialize(license_key, api_key)
|
15
|
+
@license_key = license_key
|
16
|
+
@api_key = api_key
|
17
|
+
@session_id = SecureRandom.uuid
|
18
|
+
@encryption_key = derive_key(license_key)
|
19
|
+
@integrity_checks = {}
|
20
|
+
@usage_metrics = UsageMetrics.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_license
|
24
|
+
return false if license_key.nil? || license_key.length < 32
|
25
|
+
|
26
|
+
checksum = OpenSSL::Digest::SHA256.hexdigest(license_key)
|
27
|
+
checksum.start_with?('tusk')
|
28
|
+
rescue StandardError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def encrypt_data(data)
|
33
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
34
|
+
cipher.encrypt
|
35
|
+
cipher.key = @encryption_key
|
36
|
+
iv = cipher.random_iv
|
37
|
+
|
38
|
+
encrypted = cipher.update(data) + cipher.final
|
39
|
+
auth_tag = cipher.auth_tag
|
40
|
+
|
41
|
+
result = iv + auth_tag + encrypted
|
42
|
+
Base64.strict_encode64(result)
|
43
|
+
rescue StandardError
|
44
|
+
data
|
45
|
+
end
|
46
|
+
|
47
|
+
def decrypt_data(encrypted_data)
|
48
|
+
decoded = Base64.strict_decode64(encrypted_data)
|
49
|
+
return encrypted_data if decoded.length < 28
|
50
|
+
|
51
|
+
iv = decoded[0, 12]
|
52
|
+
auth_tag = decoded[12, 16]
|
53
|
+
encrypted = decoded[28..-1]
|
54
|
+
|
55
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm')
|
56
|
+
cipher.decrypt
|
57
|
+
cipher.key = @encryption_key
|
58
|
+
cipher.iv = iv
|
59
|
+
cipher.auth_tag = auth_tag
|
60
|
+
cipher.auth_data = ''
|
61
|
+
|
62
|
+
cipher.update(encrypted) + cipher.final
|
63
|
+
rescue StandardError
|
64
|
+
encrypted_data
|
65
|
+
end
|
66
|
+
|
67
|
+
def verify_integrity(data, signature)
|
68
|
+
expected_signature = generate_signature(data)
|
69
|
+
signature == expected_signature
|
70
|
+
rescue StandardError
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_signature(data)
|
75
|
+
OpenSSL::HMAC.hexdigest('SHA256', @api_key, data)
|
76
|
+
end
|
77
|
+
|
78
|
+
def track_usage(operation, success = true)
|
79
|
+
@usage_metrics.increment_api_calls
|
80
|
+
@usage_metrics.increment_errors unless success
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_metrics
|
84
|
+
{
|
85
|
+
start_time: @usage_metrics.start_time,
|
86
|
+
api_calls: @usage_metrics.api_calls,
|
87
|
+
errors: @usage_metrics.errors,
|
88
|
+
session_id: @session_id,
|
89
|
+
uptime: Time.now.to_i - @usage_metrics.start_time
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def obfuscate_code(code)
|
94
|
+
Base64.strict_encode64(code)
|
95
|
+
end
|
96
|
+
|
97
|
+
def detect_tampering
|
98
|
+
# In production, implement file integrity checks
|
99
|
+
# For now, return true as placeholder
|
100
|
+
true
|
101
|
+
rescue StandardError
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
def report_violation(violation_type, details)
|
106
|
+
violation = Violation.new(
|
107
|
+
Time.now.to_i,
|
108
|
+
@session_id,
|
109
|
+
violation_type,
|
110
|
+
details,
|
111
|
+
@license_key[0, 8] + '...'
|
112
|
+
)
|
113
|
+
|
114
|
+
puts "SECURITY VIOLATION: #{violation}"
|
115
|
+
violation
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def derive_key(password)
|
121
|
+
salt = 'tusklang_protection_salt'
|
122
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 100_000, 32)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Inner classes
|
126
|
+
class UsageMetrics
|
127
|
+
attr_reader :start_time, :api_calls, :errors
|
128
|
+
|
129
|
+
def initialize
|
130
|
+
@start_time = Time.now.to_i
|
131
|
+
@api_calls = 0
|
132
|
+
@errors = 0
|
133
|
+
@mutex = Mutex.new
|
134
|
+
end
|
135
|
+
|
136
|
+
def increment_api_calls
|
137
|
+
@mutex.synchronize { @api_calls += 1 }
|
138
|
+
end
|
139
|
+
|
140
|
+
def increment_errors
|
141
|
+
@mutex.synchronize { @errors += 1 }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Violation
|
146
|
+
attr_reader :timestamp, :session_id, :violation_type, :details, :license_key_partial
|
147
|
+
|
148
|
+
def initialize(timestamp, session_id, violation_type, details, license_key_partial)
|
149
|
+
@timestamp = timestamp
|
150
|
+
@session_id = session_id
|
151
|
+
@violation_type = violation_type
|
152
|
+
@details = details
|
153
|
+
@license_key_partial = license_key_partial
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_s
|
157
|
+
"Violation{timestamp=#{@timestamp}, session_id='#{@session_id}', type='#{@violation_type}', details='#{@details}', license_key_partial='#{@license_key_partial}'}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Global protection instance
|
162
|
+
@protection_instance = nil
|
163
|
+
@instance_mutex = Mutex.new
|
164
|
+
|
165
|
+
class << self
|
166
|
+
def initialize_protection(license_key, api_key)
|
167
|
+
@instance_mutex.synchronize do
|
168
|
+
@protection_instance = new(license_key, api_key)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_protection
|
173
|
+
if @protection_instance.nil?
|
174
|
+
raise RuntimeError, 'Protection not initialized. Call initialize_protection() first.'
|
175
|
+
end
|
176
|
+
@protection_instance
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Shell Storage for Ruby
|
4
|
+
# Handles binary data compression and storage
|
5
|
+
|
6
|
+
module TuskLang
|
7
|
+
# Shell format storage for binary data
|
8
|
+
class ShellStorage
|
9
|
+
# Magic bytes for Shell format identification
|
10
|
+
MAGIC = 'FLEX'.bytes
|
11
|
+
VERSION = 1
|
12
|
+
|
13
|
+
# Pack data into Shell binary format
|
14
|
+
def self.pack(data)
|
15
|
+
# Compress data if it's not already compressed
|
16
|
+
compressed_data = if data[:data].is_a?(String)
|
17
|
+
Zlib::Deflate.deflate(data[:data])
|
18
|
+
elsif data[:data].is_a?(Array)
|
19
|
+
Zlib::Deflate.deflate(data[:data].pack('C*'))
|
20
|
+
else
|
21
|
+
Zlib::Deflate.deflate(data[:data].to_s)
|
22
|
+
end
|
23
|
+
|
24
|
+
id_bytes = data[:id].bytes
|
25
|
+
|
26
|
+
# Build binary format
|
27
|
+
# Magic (4) + Version (1) + ID Length (4) + ID + Data Length (4) + Data
|
28
|
+
result = []
|
29
|
+
result.concat(MAGIC)
|
30
|
+
result << VERSION
|
31
|
+
result.concat([id_bytes.length].pack('>N').bytes)
|
32
|
+
result.concat(id_bytes)
|
33
|
+
result.concat([compressed_data.length].pack('>N').bytes)
|
34
|
+
result.concat(compressed_data.bytes)
|
35
|
+
|
36
|
+
result.pack('C*')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Unpack Shell binary format
|
40
|
+
def self.unpack(shell_data)
|
41
|
+
offset = 0
|
42
|
+
|
43
|
+
# Check magic bytes
|
44
|
+
magic = shell_data.bytes[offset, 4]
|
45
|
+
raise ArgumentError, 'Invalid shell format' unless magic == MAGIC
|
46
|
+
offset += 4
|
47
|
+
|
48
|
+
# Read version
|
49
|
+
version = shell_data.bytes[offset]
|
50
|
+
raise ArgumentError, "Unsupported shell version: #{version}" unless version == VERSION
|
51
|
+
offset += 1
|
52
|
+
|
53
|
+
# Read ID
|
54
|
+
id_length = shell_data.bytes[offset, 4].pack('C*').unpack('>N')[0]
|
55
|
+
offset += 4
|
56
|
+
storage_id = shell_data.bytes[offset, id_length].pack('C*').force_encoding('UTF-8')
|
57
|
+
offset += id_length
|
58
|
+
|
59
|
+
# Read data
|
60
|
+
data_length = shell_data.bytes[offset, 4].pack('C*').unpack('>N')[0]
|
61
|
+
offset += 4
|
62
|
+
compressed_data = shell_data.bytes[offset, data_length].pack('C*')
|
63
|
+
|
64
|
+
# Decompress
|
65
|
+
data = Zlib::Inflate.inflate(compressed_data).force_encoding('UTF-8')
|
66
|
+
|
67
|
+
{
|
68
|
+
version: version,
|
69
|
+
id: storage_id,
|
70
|
+
data: data
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Detect the type of data stored in Shell format
|
75
|
+
def self.detect_type(shell_data)
|
76
|
+
data = unpack(shell_data)
|
77
|
+
content = data[:data].to_s
|
78
|
+
|
79
|
+
# Try to detect JSON
|
80
|
+
return 'json' if content.start_with?('{') || content.start_with?('[')
|
81
|
+
|
82
|
+
# Try to detect TSK format
|
83
|
+
return 'tsk' if content.include?('[') && content.include?(']') && content.include?('=')
|
84
|
+
|
85
|
+
# Try to detect XML
|
86
|
+
return 'xml' if content.start_with?('<') && content.include?('>')
|
87
|
+
|
88
|
+
# Default to text
|
89
|
+
'text'
|
90
|
+
rescue
|
91
|
+
'unknown'
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create Shell data with metadata
|
95
|
+
def self.create_shell_data(data, id, metadata = nil)
|
96
|
+
{
|
97
|
+
version: VERSION,
|
98
|
+
id: id,
|
99
|
+
data: data,
|
100
|
+
metadata: metadata || {}
|
101
|
+
}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|