excavate 1.0.3 → 1.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 +4 -4
- data/.rubocop_todo.yml +6 -94
- data/TODO.fixup/01-autoload-migration.md +65 -0
- data/TODO.fixup/02-redundant-external-requires.md +48 -0
- data/TODO.fixup/03-extractor-registry-ocp.md +150 -0
- data/TODO.fixup/04-filemagic-signature-struct.md +68 -0
- data/TODO.fixup/05-utils-silence-stream-dead-code.md +57 -0
- data/TODO.fixup/06-archive-filesystem-module.md +72 -0
- data/TODO.fixup/07-archive-targets-helper.md +62 -0
- data/TODO.fixup/08-archive-selection-dry.md +133 -0
- data/TODO.fixup/09-cab-fallback-heuristic-named.md +88 -0
- data/TODO.fixup/10-specs-remove-respond-to-matcher.md +38 -0
- data/TODO.fixup/11-specs-cover-new-collaborators.md +101 -0
- data/TODO.fixup/12-final-verification.md +20 -0
- data/TODO.fixup/README.md +73 -0
- data/excavate.gemspec +1 -1
- data/lib/excavate/archive.rb +46 -186
- data/lib/excavate/cli.rb +0 -2
- data/lib/excavate/extractors/cab_extractor.rb +2 -0
- data/lib/excavate/extractors/cpio_extractor.rb +3 -1
- data/lib/excavate/extractors/extractor.rb +72 -1
- data/lib/excavate/extractors/gzip_extractor.rb +4 -6
- data/lib/excavate/extractors/ole_extractor.rb +2 -2
- data/lib/excavate/extractors/rpm_extractor.rb +2 -1
- data/lib/excavate/extractors/seven_zip_extractor.rb +2 -2
- data/lib/excavate/extractors/tar_extractor.rb +2 -0
- data/lib/excavate/extractors/xar_extractor.rb +2 -1
- data/lib/excavate/extractors/xz_extractor.rb +3 -58
- data/lib/excavate/extractors/zip_extractor.rb +2 -0
- data/lib/excavate/extractors.rb +17 -11
- data/lib/excavate/file_magic.rb +30 -13
- data/lib/excavate/filesystem.rb +70 -0
- data/lib/excavate/nested_cab_fallback.rb +31 -0
- data/lib/excavate/selection.rb +64 -0
- data/lib/excavate/version.rb +1 -1
- data/lib/excavate.rb +13 -5
- metadata +20 -5
- data/lib/excavate/utils.rb +0 -14
data/lib/excavate/archive.rb
CHANGED
|
@@ -1,48 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Excavate
|
|
2
4
|
class Archive
|
|
3
|
-
INVALID_MEMORY_MESSAGE =
|
|
4
|
-
"invalid memory read at address=0x0000000000000000".freeze
|
|
5
|
-
|
|
6
|
-
TYPES = { "cab" => Extractors::CabExtractor,
|
|
7
|
-
"cpio" => Extractors::CpioExtractor,
|
|
8
|
-
"exe" => Extractors::SevenZipExtractor,
|
|
9
|
-
"gz" => Extractors::GzipExtractor,
|
|
10
|
-
"msi" => Extractors::OleExtractor,
|
|
11
|
-
"pkg" => Extractors::XarExtractor,
|
|
12
|
-
"rpm" => Extractors::RpmExtractor,
|
|
13
|
-
"tar" => Extractors::TarExtractor,
|
|
14
|
-
"xz" => Extractors::XzExtractor,
|
|
15
|
-
"zip" => Extractors::ZipExtractor }.freeze
|
|
16
|
-
|
|
17
5
|
def initialize(archive)
|
|
18
6
|
@archive = archive
|
|
19
7
|
end
|
|
20
8
|
|
|
21
|
-
def files(recursive_packages: false, files: [], filter: nil, &)
|
|
22
|
-
|
|
9
|
+
def files(recursive_packages: false, files: [], filter: nil, &block)
|
|
10
|
+
unless block
|
|
11
|
+
return enum_for(:files, recursive_packages: recursive_packages,
|
|
12
|
+
files: files, filter: filter)
|
|
13
|
+
end
|
|
14
|
+
|
|
23
15
|
recursive_packages = true if files.any?
|
|
24
16
|
|
|
25
17
|
target = Dir.mktmpdir
|
|
26
18
|
extract(target, recursive_packages: recursive_packages,
|
|
27
19
|
files: files, filter: filter)
|
|
28
20
|
|
|
29
|
-
all_files_in(target).
|
|
21
|
+
all_files_in(target).each(&block)
|
|
30
22
|
ensure
|
|
31
|
-
|
|
23
|
+
Filesystem.remove_recursive(target) if target
|
|
32
24
|
end
|
|
33
25
|
|
|
34
26
|
def extract(target = nil,
|
|
35
27
|
recursive_packages: false,
|
|
36
28
|
files: [],
|
|
37
29
|
filter: nil)
|
|
38
|
-
# Auto-enable recursive_packages when extracting specific files
|
|
39
30
|
recursive_packages = true if files.any?
|
|
40
31
|
|
|
41
32
|
if files.size.positive?
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
extract_selection(target, Selection.from_files(files),
|
|
34
|
+
recursive_packages: recursive_packages)
|
|
44
35
|
elsif filter
|
|
45
|
-
|
|
36
|
+
extract_selection(target, Selection.from_filter(filter),
|
|
46
37
|
recursive_packages: recursive_packages)
|
|
47
38
|
else
|
|
48
39
|
extract_all(target, recursive_packages: recursive_packages)
|
|
@@ -51,20 +42,20 @@ module Excavate
|
|
|
51
42
|
|
|
52
43
|
private
|
|
53
44
|
|
|
54
|
-
def
|
|
45
|
+
def extract_selection(target, selection, recursive_packages: false)
|
|
55
46
|
tmp = Dir.mktmpdir
|
|
56
47
|
extract_all(tmp, recursive_packages: recursive_packages)
|
|
57
|
-
|
|
58
|
-
copy_files(
|
|
48
|
+
found = selection.match(all_files_in(tmp), tmp)
|
|
49
|
+
copy_files(found, target || Dir.pwd)
|
|
59
50
|
ensure
|
|
60
|
-
|
|
51
|
+
Filesystem.remove_recursive(tmp) if tmp
|
|
61
52
|
end
|
|
62
53
|
|
|
63
54
|
def copy_files(files, target)
|
|
55
|
+
FileUtils.mkdir_p(target)
|
|
64
56
|
files.map do |file|
|
|
65
|
-
FileUtils.mkdir_p(target)
|
|
66
57
|
target_path = File.join(target, File.basename(file))
|
|
67
|
-
|
|
58
|
+
ensure_target_absent(target_path)
|
|
68
59
|
|
|
69
60
|
FileUtils.cp(file, target_path)
|
|
70
61
|
|
|
@@ -72,69 +63,10 @@ module Excavate
|
|
|
72
63
|
end
|
|
73
64
|
end
|
|
74
65
|
|
|
75
|
-
def ensure_not_exist(path)
|
|
76
|
-
if File.exist?(path)
|
|
77
|
-
type = File.directory?(path) ? "directory" : "file"
|
|
78
|
-
raise(TargetExistsError,
|
|
79
|
-
"Target #{type} `#{File.basename(path)}` already exists.")
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def find_files(source, files)
|
|
84
|
-
all_files = all_files_in(source)
|
|
85
|
-
|
|
86
|
-
files.map do |target_file|
|
|
87
|
-
found_file = all_files.find do |source_file|
|
|
88
|
-
file_matches?(source_file, target_file, source)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
unless found_file
|
|
92
|
-
raise(TargetNotFoundError, "File `#{target_file}` not found.")
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
found_file
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def file_matches?(source_file, target_file, source_dir)
|
|
100
|
-
base_path(source_file, source_dir) == target_file
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def base_path(path, prefix)
|
|
104
|
-
path.sub(prefix, "").sub(/^\//, "").sub(/^\\/, "")
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def extract_by_filter(target, filter, recursive_packages: false)
|
|
108
|
-
tmp = Dir.mktmpdir
|
|
109
|
-
extract_all(tmp, recursive_packages: recursive_packages)
|
|
110
|
-
found_files = find_by_filter(tmp, filter)
|
|
111
|
-
copy_files(found_files, target || Dir.pwd)
|
|
112
|
-
ensure
|
|
113
|
-
FileUtils.rm_rf(tmp)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def find_by_filter(source, filter)
|
|
117
|
-
all_files = all_files_in(source)
|
|
118
|
-
|
|
119
|
-
found_files = all_files.select do |source_file|
|
|
120
|
-
file_matches_filter?(source_file, filter, source)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
if found_files.empty?
|
|
124
|
-
raise(TargetNotFoundError, "Filter `#{filter}` matched no file.")
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
found_files
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def file_matches_filter?(source_file, filter, source_dir)
|
|
131
|
-
File.fnmatch?(filter, base_path(source_file, source_dir))
|
|
132
|
-
end
|
|
133
|
-
|
|
134
66
|
def extract_all(target, recursive_packages: false)
|
|
135
67
|
source = File.expand_path(@archive)
|
|
136
68
|
target ||= default_target(source)
|
|
137
|
-
|
|
69
|
+
ensure_target_empty(target)
|
|
138
70
|
|
|
139
71
|
if recursive_packages
|
|
140
72
|
extract_recursively(source, target)
|
|
@@ -145,19 +77,25 @@ module Excavate
|
|
|
145
77
|
target
|
|
146
78
|
end
|
|
147
79
|
|
|
148
|
-
def
|
|
149
|
-
unless
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
80
|
+
def ensure_target_absent(path)
|
|
81
|
+
return unless File.exist?(path)
|
|
82
|
+
|
|
83
|
+
kind = File.directory?(path) ? "directory" : "file"
|
|
84
|
+
raise TargetExistsError,
|
|
85
|
+
"Target #{kind} `#{File.basename(path)}` already exists."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ensure_target_empty(path)
|
|
89
|
+
return if Dir.empty?(path)
|
|
90
|
+
|
|
91
|
+
raise TargetNotEmptyError,
|
|
92
|
+
"Target directory `#{File.basename(path)}` is not empty."
|
|
153
93
|
end
|
|
154
94
|
|
|
155
95
|
def default_target(source)
|
|
156
96
|
target = File.expand_path(File.basename(source, ".*"))
|
|
157
|
-
|
|
158
|
-
|
|
97
|
+
ensure_target_absent(target)
|
|
159
98
|
FileUtils.mkdir(target)
|
|
160
|
-
|
|
161
99
|
target
|
|
162
100
|
end
|
|
163
101
|
|
|
@@ -175,7 +113,7 @@ module Excavate
|
|
|
175
113
|
if File.directory?(archive)
|
|
176
114
|
duplicate_dir(archive, target)
|
|
177
115
|
elsif !archive?(archive)
|
|
178
|
-
|
|
116
|
+
FileUtils.cp(archive, target)
|
|
179
117
|
else
|
|
180
118
|
extract_once(archive, target)
|
|
181
119
|
end
|
|
@@ -189,27 +127,17 @@ module Excavate
|
|
|
189
127
|
end
|
|
190
128
|
end
|
|
191
129
|
|
|
192
|
-
def copy_file(archive, target)
|
|
193
|
-
FileUtils.cp(archive, target)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def may_be_nested_cab?(extension, message)
|
|
197
|
-
extension == "exe" &&
|
|
198
|
-
(message.start_with?("Invalid file format",
|
|
199
|
-
"Unrecognized archive format") ||
|
|
200
|
-
message.include?("Invalid .7z signature"))
|
|
201
|
-
end
|
|
202
|
-
|
|
203
130
|
def extract_once(archive, target)
|
|
204
|
-
|
|
205
|
-
extractor_class =
|
|
131
|
+
type = FileMagic.detect(archive)
|
|
132
|
+
extractor_class = Extractors::Extractor.for_magic_type(type)
|
|
206
133
|
unless extractor_class
|
|
207
|
-
raise
|
|
134
|
+
raise UnknownArchiveError,
|
|
135
|
+
"Could not unarchive `#{archive}`."
|
|
208
136
|
end
|
|
209
137
|
|
|
210
138
|
extractor_class.new(archive).extract(target)
|
|
211
139
|
rescue StandardError => e
|
|
212
|
-
raise unless
|
|
140
|
+
raise unless NestedCabFallback.applies_to?(type, e)
|
|
213
141
|
|
|
214
142
|
Extractors::CabExtractor.new(archive).extract(target)
|
|
215
143
|
end
|
|
@@ -217,81 +145,15 @@ module Excavate
|
|
|
217
145
|
def extract_and_replace(archive)
|
|
218
146
|
target = Dir.mktmpdir
|
|
219
147
|
extract_recursively(archive, target)
|
|
220
|
-
|
|
148
|
+
Filesystem.replace_with_contents(archive, target)
|
|
221
149
|
rescue StandardError
|
|
222
150
|
# During recursive extraction of nested archives, silently skip
|
|
223
151
|
# any that fail (e.g. .msi files that aren't real OLE, .cab files
|
|
224
152
|
# with incompatible format, .exe files with unsupported compression).
|
|
225
|
-
# Only re-raise
|
|
226
|
-
raise unless
|
|
153
|
+
# Only re-raise if the file is not a recognized archive format.
|
|
154
|
+
raise unless File.exist?(archive) && archive?(archive)
|
|
227
155
|
ensure
|
|
228
|
-
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def replace_archive_with_contents(archive, target)
|
|
232
|
-
windows_safe_rm(archive)
|
|
233
|
-
FileUtils.mv(target, archive)
|
|
234
|
-
rescue Errno::EACCES
|
|
235
|
-
# Windows: file is locked. Copy extracted contents to archive location
|
|
236
|
-
# and keep both the archive and extracted files
|
|
237
|
-
target_dir = File.dirname(archive)
|
|
238
|
-
# Copy all extracted files to the target directory
|
|
239
|
-
Dir.glob(File.join(target, "**", "*")).each do |src|
|
|
240
|
-
next unless File.file?(src)
|
|
241
|
-
|
|
242
|
-
dest = File.join(target_dir, File.basename(src))
|
|
243
|
-
FileUtils.cp(src, dest) unless File.exist?(dest)
|
|
244
|
-
end
|
|
245
|
-
# Leave the original locked archive in place
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Windows sometimes holds file locks briefly after operations.
|
|
249
|
-
# This method retries file deletion with a small delay.
|
|
250
|
-
def windows_safe_rm(path, max_retries: 5)
|
|
251
|
-
attempts = 0
|
|
252
|
-
begin
|
|
253
|
-
FileUtils.rm(path)
|
|
254
|
-
rescue Errno::EACCES => e
|
|
255
|
-
attempts += 1
|
|
256
|
-
if attempts < max_retries
|
|
257
|
-
sleep(0.2)
|
|
258
|
-
retry
|
|
259
|
-
else
|
|
260
|
-
raise e
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Windows-safe recursive removal
|
|
266
|
-
def windows_safe_rm_rf(path, max_retries: 5)
|
|
267
|
-
attempts = 0
|
|
268
|
-
begin
|
|
269
|
-
FileUtils.rm_rf(path)
|
|
270
|
-
rescue Errno::EACCES, Errno::ENOTEMPTY => e
|
|
271
|
-
attempts += 1
|
|
272
|
-
if attempts < max_retries
|
|
273
|
-
sleep(0.2)
|
|
274
|
-
retry
|
|
275
|
-
else
|
|
276
|
-
raise e
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def normalized_extension(file)
|
|
282
|
-
fetch_extension(file).downcase
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def fetch_extension(file)
|
|
286
|
-
File.extname(filename(file)).sub(/^\./, "")
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def filename(file)
|
|
290
|
-
if file.respond_to?(:original_filename)
|
|
291
|
-
file.original_filename
|
|
292
|
-
else
|
|
293
|
-
File.basename(file)
|
|
294
|
-
end
|
|
156
|
+
Filesystem.remove_recursive(target) if target
|
|
295
157
|
end
|
|
296
158
|
|
|
297
159
|
def all_files_in(dir)
|
|
@@ -301,10 +163,8 @@ module Excavate
|
|
|
301
163
|
def archive?(file)
|
|
302
164
|
return false unless File.file?(file)
|
|
303
165
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
TYPES.key?(ext)
|
|
166
|
+
type = FileMagic.detect(file)
|
|
167
|
+
!type.nil? && !Extractors::Extractor.for_magic_type(type).nil?
|
|
308
168
|
end
|
|
309
169
|
end
|
|
310
170
|
end
|
data/lib/excavate/cli.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "omnizip
|
|
3
|
+
require "omnizip"
|
|
4
4
|
|
|
5
5
|
module Excavate
|
|
6
6
|
module Extractors
|
|
7
7
|
class CpioExtractor < Extractor
|
|
8
|
+
handles :cpio
|
|
9
|
+
|
|
8
10
|
def extract(target)
|
|
9
11
|
reader = Omnizip::Formats::Cpio::Reader.new(@archive)
|
|
10
12
|
reader.open
|
|
@@ -1,12 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Excavate
|
|
2
4
|
module Extractors
|
|
5
|
+
# Base class for all archive extractors.
|
|
6
|
+
#
|
|
7
|
+
# Subclasses register the magic-byte types they handle by calling
|
|
8
|
+
# `handles :type, ...` at class-body load time. The base class
|
|
9
|
+
# maintains the registry; new formats are added by creating a new
|
|
10
|
+
# subclass file with a `handles` declaration, with no edits to this
|
|
11
|
+
# file or to any branching logic.
|
|
3
12
|
class Extractor
|
|
13
|
+
@@registry = {}
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Declare which magic-byte types this extractor handles.
|
|
17
|
+
# Called at class-body load time in each subclass.
|
|
18
|
+
def handles(*magic_types)
|
|
19
|
+
magic_types.each { |type| @@registry[type] = self }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Resolve the extractor class for a magic-byte type, or nil if
|
|
23
|
+
# no extractor is registered. Triggers eager load of all
|
|
24
|
+
# extractor subclasses on first miss so every `handles`
|
|
25
|
+
# declaration has had a chance to fire.
|
|
26
|
+
def for_magic_type(type)
|
|
27
|
+
return @@registry[type] if @@registry.key?(type)
|
|
28
|
+
|
|
29
|
+
eager_load_subclasses!
|
|
30
|
+
@@registry[type]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The full type → class registry. Exposed for introspection
|
|
34
|
+
# (specs, debugging). Not for mutation.
|
|
35
|
+
def registry
|
|
36
|
+
@@registry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def registered_types
|
|
40
|
+
@@registry.keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Force-load every autoloaded constant under Extractors so that
|
|
44
|
+
# each subclass's `handles` declaration runs. Idempotent.
|
|
45
|
+
def eager_load_subclasses!
|
|
46
|
+
Extractors.constants.each { |name| Extractors.const_get(name) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
4
50
|
def initialize(archive)
|
|
5
51
|
@archive = archive
|
|
6
52
|
end
|
|
7
53
|
|
|
8
54
|
def extract(_target)
|
|
9
|
-
raise NotImplementedError
|
|
55
|
+
raise NotImplementedError, "you must implement #extract"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Detect inner format of decompressed data and extract it
|
|
61
|
+
# recursively, or write raw output when no extractor matches.
|
|
62
|
+
# Shared by GzipExtractor and XzExtractor.
|
|
63
|
+
def extract_inner(data, target)
|
|
64
|
+
inner_type = FileMagic.detect_bytes(data)
|
|
65
|
+
extractor_class = self.class.for_magic_type(inner_type) if inner_type
|
|
66
|
+
|
|
67
|
+
if extractor_class
|
|
68
|
+
temp = File.join(target, ".temp_#{Time.now.to_i}_#{rand(1000)}")
|
|
69
|
+
File.binwrite(temp, data)
|
|
70
|
+
extractor_class.new(temp).extract(target)
|
|
71
|
+
else
|
|
72
|
+
write_raw_output(data, target)
|
|
73
|
+
end
|
|
74
|
+
ensure
|
|
75
|
+
FileUtils.rm_f(temp) if temp
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write_raw_output(data, target)
|
|
79
|
+
basename = File.basename(@archive, ".*")
|
|
80
|
+
File.binwrite(File.join(target, basename), data)
|
|
10
81
|
end
|
|
11
82
|
end
|
|
12
83
|
end
|
|
@@ -5,13 +5,11 @@ require "zlib"
|
|
|
5
5
|
module Excavate
|
|
6
6
|
module Extractors
|
|
7
7
|
class GzipExtractor < Extractor
|
|
8
|
-
|
|
9
|
-
basename = File.basename(@archive, ".*")
|
|
10
|
-
output_path = File.join(target, basename)
|
|
8
|
+
handles :gzip
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
def extract(target)
|
|
11
|
+
data = Zlib::GzipReader.open(@archive, &:read)
|
|
12
|
+
extract_inner(data, target)
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
end
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "omnizip"
|
|
4
|
-
require "omnizip/formats/ole"
|
|
5
|
-
require_relative "../file_magic"
|
|
6
4
|
|
|
7
5
|
module Excavate
|
|
8
6
|
module Extractors
|
|
@@ -10,6 +8,8 @@ module Excavate
|
|
|
10
8
|
#
|
|
11
9
|
# Uses Omnizip's OLE format support for extraction.
|
|
12
10
|
class OleExtractor < Extractor
|
|
11
|
+
handles :ole
|
|
12
|
+
|
|
13
13
|
def extract(target)
|
|
14
14
|
do_extract(target)
|
|
15
15
|
rename_archives(target)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "omnizip"
|
|
4
|
-
require "omnizip/formats/rpm"
|
|
5
4
|
|
|
6
5
|
module Excavate
|
|
7
6
|
module Extractors
|
|
@@ -10,6 +9,8 @@ module Excavate
|
|
|
10
9
|
# Uses Omnizip's RPM format support for extraction.
|
|
11
10
|
# Extracts the raw payload as a file (e.g., fonts.src.cpio.gz).
|
|
12
11
|
class RpmExtractor < Extractor
|
|
12
|
+
handles :rpm
|
|
13
|
+
|
|
13
14
|
def extract(target)
|
|
14
15
|
rpm = Omnizip::Formats::Rpm::Reader.new(@archive)
|
|
15
16
|
rpm.open
|
|
@@ -5,8 +5,9 @@ require "omnizip"
|
|
|
5
5
|
module Excavate
|
|
6
6
|
module Extractors
|
|
7
7
|
class SevenZipExtractor < Extractor
|
|
8
|
+
handles :seven_zip, :exe
|
|
9
|
+
|
|
8
10
|
def extract(target)
|
|
9
|
-
# Check for embedded 7z in self-extracting archives
|
|
10
11
|
offset = Omnizip::Formats::SevenZip.search_embedded(@archive)
|
|
11
12
|
|
|
12
13
|
if offset
|
|
@@ -15,7 +16,6 @@ module Excavate
|
|
|
15
16
|
reader.extract_all(target)
|
|
16
17
|
end
|
|
17
18
|
else
|
|
18
|
-
# Regular 7z archive
|
|
19
19
|
Omnizip::Formats::SevenZip.open(@archive) do |reader|
|
|
20
20
|
reader.extract_all(target)
|
|
21
21
|
end
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "omnizip"
|
|
4
|
-
require "omnizip/formats/xar"
|
|
5
4
|
|
|
6
5
|
module Excavate
|
|
7
6
|
module Extractors
|
|
8
7
|
class XarExtractor < Extractor
|
|
8
|
+
handles :xar
|
|
9
|
+
|
|
9
10
|
def extract(target)
|
|
10
11
|
Omnizip::Formats::Xar.extract(@archive, target)
|
|
11
12
|
rename_payload(target)
|
|
@@ -1,70 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "omnizip"
|
|
4
|
-
require "zlib"
|
|
5
4
|
|
|
6
5
|
module Excavate
|
|
7
6
|
module Extractors
|
|
8
|
-
# Extractor for XZ compressed archives (both .xz and .tar.xz formats)
|
|
9
|
-
#
|
|
10
|
-
# This extractor handles:
|
|
11
|
-
# - Pure XZ compressed files (.xz)
|
|
12
|
-
# - Compound TAR+XZ archives (.tar.xz)
|
|
13
|
-
#
|
|
14
|
-
# Uses Omnizip for XZ decompression.
|
|
15
7
|
class XzExtractor < Extractor
|
|
16
|
-
|
|
17
|
-
if tar_xz?
|
|
18
|
-
extract_tar_xz(target)
|
|
19
|
-
else
|
|
20
|
-
extract_pure_xz(target)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def tar_xz?
|
|
27
|
-
@archive.end_with?(".tar.xz", ".txz")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def extract_tar_xz(target)
|
|
31
|
-
data = Omnizip::Formats::Xz.decompress(@archive)
|
|
32
|
-
data = strip_compression(data)
|
|
33
|
-
validate_tar!(data)
|
|
34
|
-
|
|
35
|
-
# Write tar file and extract
|
|
36
|
-
temp_tar = File.join(target, ".temp_#{Time.now.to_i}_#{rand(1000)}.tar")
|
|
37
|
-
File.binwrite(temp_tar, data)
|
|
38
|
-
|
|
39
|
-
TarExtractor.new(temp_tar).extract(target)
|
|
40
|
-
ensure
|
|
41
|
-
File.delete(temp_tar) if temp_tar && File.exist?(temp_tar)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def strip_compression(data)
|
|
45
|
-
if FileMagic.detect_bytes(data) == :gzip
|
|
46
|
-
return Zlib::GzipReader.new(StringIO.new(data)).read
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
data
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def validate_tar!(data)
|
|
53
|
-
inner_type = FileMagic.detect_bytes(data)
|
|
54
|
-
return if inner_type == :tar
|
|
8
|
+
handles :xz
|
|
55
9
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
raise UnknownArchiveError,
|
|
59
|
-
"Expected tar inside #{@archive}, got #{inner_type}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def extract_pure_xz(target)
|
|
63
|
-
# Decompress XZ
|
|
10
|
+
def extract(target)
|
|
64
11
|
data = Omnizip::Formats::Xz.decompress(@archive)
|
|
65
|
-
|
|
66
|
-
output_path = File.join(target, basename)
|
|
67
|
-
File.binwrite(output_path, data)
|
|
12
|
+
extract_inner(data, target)
|
|
68
13
|
end
|
|
69
14
|
end
|
|
70
15
|
end
|
data/lib/excavate/extractors.rb
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Excavate
|
|
4
|
+
module Extractors
|
|
5
|
+
autoload :Extractor, "excavate/extractors/extractor"
|
|
6
|
+
autoload :CabExtractor, "excavate/extractors/cab_extractor"
|
|
7
|
+
autoload :CpioExtractor, "excavate/extractors/cpio_extractor"
|
|
8
|
+
autoload :GzipExtractor, "excavate/extractors/gzip_extractor"
|
|
9
|
+
autoload :OleExtractor, "excavate/extractors/ole_extractor"
|
|
10
|
+
autoload :RpmExtractor, "excavate/extractors/rpm_extractor"
|
|
11
|
+
autoload :SevenZipExtractor, "excavate/extractors/seven_zip_extractor"
|
|
12
|
+
autoload :TarExtractor, "excavate/extractors/tar_extractor"
|
|
13
|
+
autoload :XarExtractor, "excavate/extractors/xar_extractor"
|
|
14
|
+
autoload :XzExtractor, "excavate/extractors/xz_extractor"
|
|
15
|
+
autoload :ZipExtractor, "excavate/extractors/zip_extractor"
|
|
16
|
+
end
|
|
17
|
+
end
|