keysloth 0.1.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/.keyslothrc.example +14 -0
- data/.rspec +4 -0
- data/.rubocop.yml +98 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +45 -0
- data/Makefile +193 -0
- data/README.md +448 -0
- data/Rakefile +99 -0
- data/bin/keysloth +16 -0
- data/keysloth.gemspec +51 -0
- data/lib/keysloth/cli.rb +468 -0
- data/lib/keysloth/config.rb +129 -0
- data/lib/keysloth/crypto.rb +304 -0
- data/lib/keysloth/errors.rb +41 -0
- data/lib/keysloth/file_manager.rb +450 -0
- data/lib/keysloth/git_manager.rb +394 -0
- data/lib/keysloth/logger.rb +172 -0
- data/lib/keysloth/version.rb +6 -0
- data/lib/keysloth.rb +275 -0
- data/task/cr.md +59 -0
- data/task/plan.md +169 -0
- data/task/ragged_removing.md +103 -0
- data/task/task.md +43 -0
- data/task/test_plan.md +266 -0
- metadata +174 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module KeySloth
|
|
7
|
+
# Класс для управления файловыми операциями KeySloth
|
|
8
|
+
#
|
|
9
|
+
# Обеспечивает создание директорий, чтение и запись файлов,
|
|
10
|
+
# создание резервных копий и валидацию путей.
|
|
11
|
+
# Поддерживает работу с различными типами секретных файлов.
|
|
12
|
+
#
|
|
13
|
+
# @example Использование
|
|
14
|
+
# file_manager = KeySloth::FileManager.new
|
|
15
|
+
#
|
|
16
|
+
# # Создание директории
|
|
17
|
+
# file_manager.ensure_directory('./secrets')
|
|
18
|
+
#
|
|
19
|
+
# # Сбор файлов секретов
|
|
20
|
+
# files = file_manager.collect_secret_files('./secrets')
|
|
21
|
+
#
|
|
22
|
+
# # Создание backup'а
|
|
23
|
+
# file_manager.create_backup('./secrets')
|
|
24
|
+
#
|
|
25
|
+
# @author KeySloth Team
|
|
26
|
+
# @since 0.1.0
|
|
27
|
+
class FileManager
|
|
28
|
+
# Поддерживаемые расширения файлов секретов
|
|
29
|
+
SECRET_FILE_EXTENSIONS = %w[.cer .p12 .mobileprovisioning .json].freeze
|
|
30
|
+
|
|
31
|
+
# Максимальное количество backup'ов
|
|
32
|
+
DEFAULT_BACKUP_COUNT = 3
|
|
33
|
+
|
|
34
|
+
# Инициализация файлового менеджера
|
|
35
|
+
#
|
|
36
|
+
# @param logger [KeySloth::Logger] Логгер для вывода сообщений
|
|
37
|
+
# @param backup_count [Integer] Количество backup'ов для хранения
|
|
38
|
+
def initialize(logger = nil, backup_count = DEFAULT_BACKUP_COUNT)
|
|
39
|
+
@logger = logger || Logger.new(level: :error)
|
|
40
|
+
@backup_count = backup_count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Обеспечивает существование директории
|
|
44
|
+
#
|
|
45
|
+
# @param path [String] Путь к директории
|
|
46
|
+
# @raise [FileSystemError] при ошибках создания директории
|
|
47
|
+
def ensure_directory(path)
|
|
48
|
+
return if directory_exists?(path)
|
|
49
|
+
|
|
50
|
+
@logger.info("Создаем директорию: #{path}")
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
FileUtils.mkdir_p(path)
|
|
54
|
+
@logger.debug("Директория успешно создана: #{path}")
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
@logger.error('Ошибка создания директории', e)
|
|
57
|
+
raise FileSystemError, "Не удалось создать директорию #{path}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Проверяет существование директории
|
|
62
|
+
#
|
|
63
|
+
# @param path [String] Путь к директории
|
|
64
|
+
# @return [Boolean] true если директория существует
|
|
65
|
+
def directory_exists?(path)
|
|
66
|
+
File.directory?(path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Читает содержимое файла
|
|
70
|
+
#
|
|
71
|
+
# @param file_path [String] Путь к файлу
|
|
72
|
+
# @return [String] Содержимое файла
|
|
73
|
+
# @raise [FileSystemError] при ошибках чтения файла
|
|
74
|
+
def read_file(file_path)
|
|
75
|
+
@logger.debug("Читаем файл: #{file_path}")
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
content = File.read(file_path)
|
|
79
|
+
@logger.debug("Файл прочитан успешно (размер: #{content.length} байт)")
|
|
80
|
+
content
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
@logger.error('Ошибка чтения файла', e)
|
|
83
|
+
raise FileSystemError, "Не удалось прочитать файл #{file_path}: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Записывает содержимое в файл
|
|
88
|
+
#
|
|
89
|
+
# @param file_path [String] Путь к файлу
|
|
90
|
+
# @param content [String] Содержимое для записи
|
|
91
|
+
# @raise [FileSystemError] при ошибках записи файла
|
|
92
|
+
def write_file(file_path, content)
|
|
93
|
+
@logger.debug("Записываем файл: #{file_path}")
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
# Создаем директорию если не существует
|
|
97
|
+
directory = File.dirname(file_path)
|
|
98
|
+
ensure_directory(directory) unless directory_exists?(directory)
|
|
99
|
+
|
|
100
|
+
File.write(file_path, content)
|
|
101
|
+
@logger.debug("Файл записан успешно (размер: #{content.length} байт)")
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
@logger.error('Ошибка записи файла', e)
|
|
104
|
+
raise FileSystemError, "Не удалось записать файл #{file_path}: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Собирает все файлы секретов из директории
|
|
109
|
+
#
|
|
110
|
+
# @param directory_path [String] Путь к директории с секретами
|
|
111
|
+
# @return [Array<String>] Массив путей к файлам секретов
|
|
112
|
+
# @raise [FileSystemError] при ошибках доступа к директории
|
|
113
|
+
def collect_secret_files(directory_path)
|
|
114
|
+
@logger.debug("Собираем файлы секретов из: #{directory_path}")
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
validate_directory_access!(directory_path)
|
|
118
|
+
|
|
119
|
+
files = []
|
|
120
|
+
SECRET_FILE_EXTENSIONS.each do |extension|
|
|
121
|
+
pattern = File.join(directory_path, "**/*#{extension}")
|
|
122
|
+
matching_files = Dir.glob(pattern)
|
|
123
|
+
files.concat(matching_files)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@logger.info("Найдено #{files.size} файлов секретов")
|
|
127
|
+
files.sort
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
@logger.error('Ошибка сбора файлов секретов', e)
|
|
130
|
+
raise FileSystemError, "Не удалось собрать файлы секретов: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Возвращает относительный путь файла
|
|
135
|
+
#
|
|
136
|
+
# @param file_path [String] Полный путь к файлу
|
|
137
|
+
# @param base_path [String] Базовый путь
|
|
138
|
+
# @return [String] Относительный путь
|
|
139
|
+
def get_relative_path(file_path, base_path)
|
|
140
|
+
Pathname.new(file_path).relative_path_from(Pathname.new(base_path)).to_s
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Создает резервную копию директории
|
|
144
|
+
#
|
|
145
|
+
# @param directory_path [String] Путь к директории для backup'а
|
|
146
|
+
# @return [String, nil] Путь к созданному backup'у или nil если директория не существует
|
|
147
|
+
# @raise [FileSystemError] при ошибках создания backup'а
|
|
148
|
+
def create_backup(directory_path)
|
|
149
|
+
return nil unless directory_exists?(directory_path)
|
|
150
|
+
|
|
151
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
152
|
+
backup_name = "#{File.basename(directory_path)}_backup_#{timestamp}"
|
|
153
|
+
backup_path = File.join(File.dirname(directory_path), backup_name)
|
|
154
|
+
|
|
155
|
+
@logger.info("Создаем backup: #{backup_path}")
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
FileUtils.cp_r(directory_path, backup_path)
|
|
159
|
+
|
|
160
|
+
# Очищаем старые backup'ы
|
|
161
|
+
cleanup_old_backups(File.dirname(directory_path), File.basename(directory_path))
|
|
162
|
+
|
|
163
|
+
@logger.debug("Backup успешно создан: #{backup_path}")
|
|
164
|
+
backup_path
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
@logger.error('Ошибка создания backup', e)
|
|
167
|
+
raise FileSystemError, "Не удалось создать backup #{directory_path}: #{e.message}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Восстанавливает из backup'а
|
|
172
|
+
#
|
|
173
|
+
# @param backup_path [String] Путь к backup'у
|
|
174
|
+
# @param target_path [String] Путь для восстановления
|
|
175
|
+
# @raise [FileSystemError] при ошибках восстановления
|
|
176
|
+
def restore_from_backup(backup_path, target_path)
|
|
177
|
+
@logger.info("Восстанавливаем из backup: #{backup_path}")
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
validate_backup_path!(backup_path)
|
|
181
|
+
|
|
182
|
+
# Удаляем существующую директорию
|
|
183
|
+
FileUtils.rm_rf(target_path)
|
|
184
|
+
|
|
185
|
+
# Копируем из backup'а
|
|
186
|
+
FileUtils.cp_r(backup_path, target_path)
|
|
187
|
+
|
|
188
|
+
@logger.info('Восстановление из backup завершено')
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
@logger.error('Ошибка восстановления из backup', e)
|
|
191
|
+
raise FileSystemError, "Не удалось восстановить из backup: #{e.message}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Возвращает список доступных backup'ов
|
|
196
|
+
#
|
|
197
|
+
# @param directory_path [String] Путь к директории
|
|
198
|
+
# @return [Array<String>] Массив путей к backup'ам (отсортированный по времени)
|
|
199
|
+
def list_backups(directory_path)
|
|
200
|
+
base_name = File.basename(directory_path)
|
|
201
|
+
parent_dir = File.dirname(directory_path)
|
|
202
|
+
backup_pattern = File.join(parent_dir, "#{base_name}_backup_*")
|
|
203
|
+
|
|
204
|
+
Dir.glob(backup_pattern)
|
|
205
|
+
.select { |path| File.directory?(path) }
|
|
206
|
+
.sort
|
|
207
|
+
.reverse # Новые backup'ы первыми
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Проверяет целостность файла секрета
|
|
211
|
+
#
|
|
212
|
+
# @param file_path [String] Путь к файлу
|
|
213
|
+
# @return [Boolean] true если файл прошел проверку целостности
|
|
214
|
+
def verify_file_integrity(file_path)
|
|
215
|
+
return false unless File.exist?(file_path)
|
|
216
|
+
return false if File.empty?(file_path)
|
|
217
|
+
|
|
218
|
+
# Базовая проверка на читаемость
|
|
219
|
+
begin
|
|
220
|
+
content = File.read(file_path, 100) # Читаем первые 100 байт для проверки
|
|
221
|
+
|
|
222
|
+
# Дополнительная проверка по типу файла
|
|
223
|
+
file_extension = File.extname(file_path).downcase
|
|
224
|
+
verify_file_type_integrity(content, file_extension)
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
@logger.debug("Ошибка проверки целостности файла #{file_path}: #{e.message}")
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Выполняет детальную проверку целостности файла
|
|
232
|
+
#
|
|
233
|
+
# @param file_path [String] Путь к файлу
|
|
234
|
+
# @return [Hash] Детальный результат проверки
|
|
235
|
+
def verify_file_integrity_detailed(file_path)
|
|
236
|
+
result = {
|
|
237
|
+
valid: false,
|
|
238
|
+
exists: false,
|
|
239
|
+
readable: false,
|
|
240
|
+
non_empty: false,
|
|
241
|
+
type_valid: false,
|
|
242
|
+
size: 0,
|
|
243
|
+
error: nil
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
begin
|
|
247
|
+
# Проверяем существование
|
|
248
|
+
result[:exists] = File.exist?(file_path)
|
|
249
|
+
return result unless result[:exists]
|
|
250
|
+
|
|
251
|
+
# Проверяем размер
|
|
252
|
+
result[:size] = File.size(file_path)
|
|
253
|
+
result[:non_empty] = result[:size].positive?
|
|
254
|
+
return result unless result[:non_empty]
|
|
255
|
+
|
|
256
|
+
# Проверяем читаемость
|
|
257
|
+
content = File.read(file_path, 200) # Читаем больше для детальной проверки
|
|
258
|
+
result[:readable] = true
|
|
259
|
+
|
|
260
|
+
# Проверяем соответствие типу файла
|
|
261
|
+
file_extension = File.extname(file_path).downcase
|
|
262
|
+
result[:type_valid] = verify_file_type_integrity(content, file_extension)
|
|
263
|
+
|
|
264
|
+
result[:valid] = result[:exists] && result[:readable] &&
|
|
265
|
+
result[:non_empty] && result[:type_valid]
|
|
266
|
+
|
|
267
|
+
@logger.debug("Детальная проверка файла #{file_path}: #{result}")
|
|
268
|
+
rescue StandardError => e
|
|
269
|
+
result[:error] = e.message
|
|
270
|
+
@logger.debug("Ошибка детальной проверки файла #{file_path}: #{e.message}")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
result
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Валидирует путь файла
|
|
277
|
+
#
|
|
278
|
+
# @param file_path [String] Путь к файлу
|
|
279
|
+
# @raise [ValidationError] при некорректном пути
|
|
280
|
+
def validate_file_path!(file_path)
|
|
281
|
+
if file_path.nil? || file_path.empty?
|
|
282
|
+
raise ValidationError, 'Путь к файлу не может быть пустым'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Проверяем на потенциально опасные пути
|
|
286
|
+
normalized_path = File.expand_path(file_path)
|
|
287
|
+
if normalized_path.include?('..')
|
|
288
|
+
raise ValidationError, 'Путь содержит потенциально опасные элементы'
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
# Валидирует доступ к директории
|
|
295
|
+
#
|
|
296
|
+
# @param directory_path [String] Путь к директории
|
|
297
|
+
# @raise [FileSystemError] при проблемах доступа
|
|
298
|
+
def validate_directory_access!(directory_path)
|
|
299
|
+
unless directory_exists?(directory_path)
|
|
300
|
+
raise FileSystemError, "Директория не существует: #{directory_path}"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
unless File.readable?(directory_path)
|
|
304
|
+
raise FileSystemError, "Директория не доступна для чтения: #{directory_path}"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Валидирует путь к backup'у
|
|
309
|
+
#
|
|
310
|
+
# @param backup_path [String] Путь к backup'у
|
|
311
|
+
# @raise [FileSystemError] при некорректном backup'е
|
|
312
|
+
def validate_backup_path!(backup_path)
|
|
313
|
+
raise FileSystemError, "Backup не существует: #{backup_path}" unless File.exist?(backup_path)
|
|
314
|
+
|
|
315
|
+
unless File.directory?(backup_path)
|
|
316
|
+
raise FileSystemError, "Backup не является директорией: #{backup_path}"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Очищает старые backup'ы
|
|
321
|
+
#
|
|
322
|
+
# @param parent_dir [String] Родительская директория
|
|
323
|
+
# @param base_name [String] Базовое имя директории
|
|
324
|
+
def cleanup_old_backups(parent_dir, base_name)
|
|
325
|
+
backups = list_backups(File.join(parent_dir, base_name))
|
|
326
|
+
|
|
327
|
+
return if backups.size <= @backup_count
|
|
328
|
+
|
|
329
|
+
# Удаляем старые backup'ы, оставляя только нужное количество
|
|
330
|
+
backups.drop(@backup_count).each do |old_backup|
|
|
331
|
+
@logger.debug("Удаляем старый backup: #{old_backup}")
|
|
332
|
+
FileUtils.remove_entry(old_backup)
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Проверяет целостность файла по его типу
|
|
337
|
+
#
|
|
338
|
+
# @param content [String] Содержимое файла (первые байты)
|
|
339
|
+
# @param file_extension [String] Расширение файла
|
|
340
|
+
# @return [Boolean] true если файл соответствует ожидаемому формату
|
|
341
|
+
def verify_file_type_integrity(content, file_extension)
|
|
342
|
+
return false if content.nil? || content.empty?
|
|
343
|
+
|
|
344
|
+
case file_extension
|
|
345
|
+
when '.cer'
|
|
346
|
+
# Проверяем сертификат (.cer файлы)
|
|
347
|
+
verify_certificate_format(content)
|
|
348
|
+
when '.p12'
|
|
349
|
+
# Проверяем PKCS#12 файлы
|
|
350
|
+
verify_p12_format(content)
|
|
351
|
+
when '.mobileprovisioning'
|
|
352
|
+
# Проверяем файлы mobile provisioning
|
|
353
|
+
verify_mobileprovisioning_format(content)
|
|
354
|
+
when '.json'
|
|
355
|
+
# Проверяем JSON файлы
|
|
356
|
+
verify_json_format(content)
|
|
357
|
+
else
|
|
358
|
+
# Для неизвестных типов - только базовая проверка
|
|
359
|
+
@logger.debug("Неизвестный тип файла: #{file_extension}")
|
|
360
|
+
true
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Проверяет формат сертификата (.cer файлы)
|
|
365
|
+
#
|
|
366
|
+
# Поддерживает проверку PEM и DER форматов сертификатов.
|
|
367
|
+
# PEM формат содержит текстовые маркеры BEGIN/END CERTIFICATE.
|
|
368
|
+
# DER формат - бинарный ASN.1, начинается с SEQUENCE tag (0x30).
|
|
369
|
+
#
|
|
370
|
+
# @param content [String] Содержимое файла сертификата
|
|
371
|
+
# @return [Boolean] true если формат корректен
|
|
372
|
+
# @example Проверка PEM сертификата
|
|
373
|
+
# content = "-----BEGIN CERTIFICATE-----\nMIIC..."
|
|
374
|
+
# verify_certificate_format(content) #=> true
|
|
375
|
+
def verify_certificate_format(content)
|
|
376
|
+
# PEM формат
|
|
377
|
+
return true if content.include?('-----BEGIN CERTIFICATE-----')
|
|
378
|
+
|
|
379
|
+
# DER формат (бинарный) - проверяем первые байты
|
|
380
|
+
# DER сертификаты начинаются с 0x30 (SEQUENCE tag)
|
|
381
|
+
return true if content.bytes.first == 0x30
|
|
382
|
+
|
|
383
|
+
@logger.debug('Файл не соответствует формату сертификата')
|
|
384
|
+
false
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Проверяет формат PKCS#12 (.p12 файлы)
|
|
388
|
+
#
|
|
389
|
+
# PKCS#12 - стандарт для хранения криптографических объектов
|
|
390
|
+
# (сертификаты + приватные ключи). Использует ASN.1 DER кодирование,
|
|
391
|
+
# поэтому файл должен начинаться с SEQUENCE tag (0x30).
|
|
392
|
+
#
|
|
393
|
+
# @param content [String] Содержимое файла PKCS#12
|
|
394
|
+
# @return [Boolean] true если формат корректен
|
|
395
|
+
# @raise [FileSystemError] при невозможности прочитать файл
|
|
396
|
+
def verify_p12_format(content)
|
|
397
|
+
# PKCS#12 файлы начинаются с 0x30 (SEQUENCE tag)
|
|
398
|
+
return true if content.bytes.first == 0x30
|
|
399
|
+
|
|
400
|
+
@logger.debug('Файл не соответствует формату PKCS#12')
|
|
401
|
+
false
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Проверяет формат Mobile Provisioning Profile (.mobileprovisioning файлы)
|
|
405
|
+
#
|
|
406
|
+
# Профили подготовки iOS могут быть в XML (текстовом) или бинарном plist формате.
|
|
407
|
+
# XML профили содержат стандартные XML/plist маркеры.
|
|
408
|
+
# Бинарные профили начинаются с 'bplist' сигнатуры.
|
|
409
|
+
#
|
|
410
|
+
# @param content [String] Содержимое файла профиля
|
|
411
|
+
# @return [Boolean] true если формат корректен
|
|
412
|
+
# @example Проверка XML профиля
|
|
413
|
+
# content = "<?xml version=\"1.0\"..."
|
|
414
|
+
# verify_mobileprovisioning_format(content) #=> true
|
|
415
|
+
def verify_mobileprovisioning_format(content)
|
|
416
|
+
# Mobile provisioning файлы содержат XML или plist структуру
|
|
417
|
+
return true if content.include?('<?xml') || content.include?('<plist')
|
|
418
|
+
|
|
419
|
+
# Могут быть в бинарном plist формате
|
|
420
|
+
return true if content.include?('bplist')
|
|
421
|
+
|
|
422
|
+
@logger.debug('Файл не соответствует формату mobile provisioning')
|
|
423
|
+
false
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Проверяет формат JSON (.json файлы)
|
|
427
|
+
#
|
|
428
|
+
# Выполняет базовую структурную проверку JSON документов.
|
|
429
|
+
# Проверяет наличие открывающих и закрывающих скобок для объектов {}
|
|
430
|
+
# и массивов []. Не выполняет полную JSON валидацию для производительности.
|
|
431
|
+
#
|
|
432
|
+
# @param content [String] Содержимое JSON файла
|
|
433
|
+
# @return [Boolean] true если базовая структура корректна
|
|
434
|
+
# @example Проверка JSON объекта
|
|
435
|
+
# content = '{"key": "value"}'
|
|
436
|
+
# verify_json_format(content) #=> true
|
|
437
|
+
# @example Проверка JSON массива
|
|
438
|
+
# content = '[{"item": 1}]'
|
|
439
|
+
# verify_json_format(content) #=> true
|
|
440
|
+
def verify_json_format(content)
|
|
441
|
+
# Базовая проверка JSON структуры
|
|
442
|
+
trimmed = content.strip
|
|
443
|
+
return true if (trimmed.start_with?('{') && trimmed.include?('}')) ||
|
|
444
|
+
(trimmed.start_with?('[') && trimmed.include?(']'))
|
|
445
|
+
|
|
446
|
+
@logger.debug('Файл не соответствует формату JSON')
|
|
447
|
+
false
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|