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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +6 -94
  3. data/TODO.fixup/01-autoload-migration.md +65 -0
  4. data/TODO.fixup/02-redundant-external-requires.md +48 -0
  5. data/TODO.fixup/03-extractor-registry-ocp.md +150 -0
  6. data/TODO.fixup/04-filemagic-signature-struct.md +68 -0
  7. data/TODO.fixup/05-utils-silence-stream-dead-code.md +57 -0
  8. data/TODO.fixup/06-archive-filesystem-module.md +72 -0
  9. data/TODO.fixup/07-archive-targets-helper.md +62 -0
  10. data/TODO.fixup/08-archive-selection-dry.md +133 -0
  11. data/TODO.fixup/09-cab-fallback-heuristic-named.md +88 -0
  12. data/TODO.fixup/10-specs-remove-respond-to-matcher.md +38 -0
  13. data/TODO.fixup/11-specs-cover-new-collaborators.md +101 -0
  14. data/TODO.fixup/12-final-verification.md +20 -0
  15. data/TODO.fixup/README.md +73 -0
  16. data/excavate.gemspec +1 -1
  17. data/lib/excavate/archive.rb +46 -186
  18. data/lib/excavate/cli.rb +0 -2
  19. data/lib/excavate/extractors/cab_extractor.rb +2 -0
  20. data/lib/excavate/extractors/cpio_extractor.rb +3 -1
  21. data/lib/excavate/extractors/extractor.rb +72 -1
  22. data/lib/excavate/extractors/gzip_extractor.rb +4 -6
  23. data/lib/excavate/extractors/ole_extractor.rb +2 -2
  24. data/lib/excavate/extractors/rpm_extractor.rb +2 -1
  25. data/lib/excavate/extractors/seven_zip_extractor.rb +2 -2
  26. data/lib/excavate/extractors/tar_extractor.rb +2 -0
  27. data/lib/excavate/extractors/xar_extractor.rb +2 -1
  28. data/lib/excavate/extractors/xz_extractor.rb +3 -58
  29. data/lib/excavate/extractors/zip_extractor.rb +2 -0
  30. data/lib/excavate/extractors.rb +17 -11
  31. data/lib/excavate/file_magic.rb +30 -13
  32. data/lib/excavate/filesystem.rb +70 -0
  33. data/lib/excavate/nested_cab_fallback.rb +31 -0
  34. data/lib/excavate/selection.rb +64 -0
  35. data/lib/excavate/version.rb +1 -1
  36. data/lib/excavate.rb +13 -5
  37. metadata +20 -5
  38. data/lib/excavate/utils.rb +0 -14
@@ -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
- # Auto-enable recursive_packages when extracting specific files
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).map(&)
21
+ all_files_in(target).each(&block)
30
22
  ensure
31
- windows_safe_rm_rf(target)
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
- extract_particular_files(target, files,
43
- recursive_packages: recursive_packages)
33
+ extract_selection(target, Selection.from_files(files),
34
+ recursive_packages: recursive_packages)
44
35
  elsif filter
45
- extract_by_filter(target, filter,
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 extract_particular_files(target, files, recursive_packages: false)
45
+ def extract_selection(target, selection, recursive_packages: false)
55
46
  tmp = Dir.mktmpdir
56
47
  extract_all(tmp, recursive_packages: recursive_packages)
57
- found_files = find_files(tmp, files)
58
- copy_files(found_files, target || Dir.pwd)
48
+ found = selection.match(all_files_in(tmp), tmp)
49
+ copy_files(found, target || Dir.pwd)
59
50
  ensure
60
- FileUtils.rm_rf(tmp)
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
- ensure_not_exist(target_path)
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
- ensure_empty(target)
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 ensure_empty(path)
149
- unless Dir.empty?(path)
150
- raise(TargetNotEmptyError,
151
- "Target directory `#{File.basename(path)}` is not empty.")
152
- end
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
- ensure_not_exist(target)
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
- copy_file(archive, target)
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
- extension = normalized_extension(archive)
205
- extractor_class = TYPES[extension]
131
+ type = FileMagic.detect(archive)
132
+ extractor_class = Extractors::Extractor.for_magic_type(type)
206
133
  unless extractor_class
207
- raise(UnknownArchiveError, "Could not unarchive `#{archive}`.")
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 may_be_nested_cab?(extension, e.message)
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
- replace_archive_with_contents(archive, target)
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 for file types we don't recognize as archives.
226
- raise unless TYPES.key?(normalized_extension(archive))
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
- FileUtils.rm_rf(target)
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
- ext = normalized_extension(file)
305
- return false if ext == "gz" && FileMagic.detect(file) != :gzip
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,7 +1,5 @@
1
1
  require "thor"
2
2
 
3
- require_relative "../excavate"
4
-
5
3
  module Excavate
6
4
  class CLI < Thor
7
5
  STATUS_SUCCESS = 0
@@ -5,6 +5,8 @@ require "cabriolet"
5
5
  module Excavate
6
6
  module Extractors
7
7
  class CabExtractor < Extractor
8
+ handles :cab
9
+
8
10
  def extract(target)
9
11
  decompressor = Cabriolet::CAB::Decompressor.new
10
12
  decompressor.salvage = true # Enable salvage mode for compatibility
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "omnizip/formats/cpio"
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.new("You must implement this method")
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
- def extract(target)
9
- basename = File.basename(@archive, ".*")
10
- output_path = File.join(target, basename)
8
+ handles :gzip
11
9
 
12
- Zlib::GzipReader.open(@archive) do |gz|
13
- File.write(output_path, gz.read, mode: "wb")
14
- end
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
@@ -5,6 +5,8 @@ require "omnizip"
5
5
  module Excavate
6
6
  module Extractors
7
7
  class TarExtractor < Extractor
8
+ handles :tar
9
+
8
10
  def extract(target)
9
11
  reader = Omnizip::Formats::Tar::Reader.open(@archive)
10
12
  reader.extract_all(target)
@@ -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
- def extract(target)
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
- inner_type ||= "unknown format"
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
- basename = File.basename(@archive, ".*")
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
@@ -5,6 +5,8 @@ require "omnizip"
5
5
  module Excavate
6
6
  module Extractors
7
7
  class ZipExtractor < Extractor
8
+ handles :zip
9
+
8
10
  def extract(target)
9
11
  reader = Omnizip::Formats::Zip::Reader.new(@archive)
10
12
  reader.read
@@ -1,11 +1,17 @@
1
- require_relative "extractors/extractor"
2
- require_relative "extractors/cab_extractor"
3
- require_relative "extractors/cpio_extractor"
4
- require_relative "extractors/gzip_extractor"
5
- require_relative "extractors/ole_extractor"
6
- require_relative "extractors/rpm_extractor"
7
- require_relative "extractors/seven_zip_extractor"
8
- require_relative "extractors/tar_extractor"
9
- require_relative "extractors/xar_extractor"
10
- require_relative "extractors/xz_extractor"
11
- require_relative "extractors/zip_extractor"
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