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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# 08 — DRY up Archive selective extraction
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Archive#extract_particular_files` and `Archive#extract_by_filter` are
|
|
8
|
+
near-duplicates:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
def extract_particular_files(target, files, recursive_packages: false)
|
|
12
|
+
tmp = Dir.mktmpdir
|
|
13
|
+
extract_all(tmp, recursive_packages: recursive_packages)
|
|
14
|
+
found_files = find_files(tmp, files)
|
|
15
|
+
copy_files(found_files, target || Dir.pwd)
|
|
16
|
+
ensure
|
|
17
|
+
FileUtils.rm_rf(tmp)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def extract_by_filter(target, filter, recursive_packages: false)
|
|
21
|
+
tmp = Dir.mktmpdir
|
|
22
|
+
extract_all(tmp, recursive_packages: recursive_packages)
|
|
23
|
+
found_files = find_by_filter(tmp, filter)
|
|
24
|
+
copy_files(found_files, target || Dir.pwd)
|
|
25
|
+
ensure
|
|
26
|
+
FileUtils.rm_rf(tmp)
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Same scaffolding (mktmpdir → extract_all → find → copy → rm_rf). The only
|
|
31
|
+
difference is the matching strategy. DRY violation.
|
|
32
|
+
|
|
33
|
+
The matching logic itself is also split across `find_files`, `find_by_filter`,
|
|
34
|
+
`file_matches?`, `file_matches_filter?`, and `base_path`. Five private
|
|
35
|
+
methods where one focused collaborator would do.
|
|
36
|
+
|
|
37
|
+
## Plan
|
|
38
|
+
|
|
39
|
+
Introduce `Excavate::Selection` — a small value object that knows how to
|
|
40
|
+
match files against either an explicit list or a glob filter.
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
module Excavate
|
|
44
|
+
class Selection
|
|
45
|
+
def self.from_files(names)
|
|
46
|
+
new(file_names: names)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.from_filter(pattern)
|
|
50
|
+
new(filter: pattern)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(file_names: nil, filter: nil)
|
|
54
|
+
@file_names = file_names
|
|
55
|
+
@filter = filter
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def match(paths, base_dir)
|
|
59
|
+
if @file_names
|
|
60
|
+
match_explicit(paths, base_dir)
|
|
61
|
+
else
|
|
62
|
+
match_filter(paths, base_dir)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def match_explicit(paths, base_dir)
|
|
69
|
+
@file_names.map do |target|
|
|
70
|
+
found = paths.find { |p| relative(p, base_dir) == target }
|
|
71
|
+
raise TargetNotFoundError, "File `#{target}` not found." unless found
|
|
72
|
+
|
|
73
|
+
found
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def match_filter(paths, base_dir)
|
|
78
|
+
matched = paths.select { |p| File.fnmatch?(@filter, relative(p, base_dir)) }
|
|
79
|
+
raise TargetNotFoundError, "Filter `#{@filter}` matched no file." if matched.empty?
|
|
80
|
+
|
|
81
|
+
matched
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def relative(path, base_dir)
|
|
85
|
+
path.sub(base_dir, "").sub(%r{^/}, "").sub(/^\\/, "")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Update `lib/excavate.rb` to autoload `:Selection, "excavate/selection"`.
|
|
92
|
+
|
|
93
|
+
Update `Archive`:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
def extract(target = nil, recursive_packages: false, files: [], filter: nil)
|
|
97
|
+
recursive_packages = true if files.any?
|
|
98
|
+
|
|
99
|
+
if files.size.positive?
|
|
100
|
+
extract_selection(target, Selection.from_files(files), recursive_packages: recursive_packages)
|
|
101
|
+
elsif filter
|
|
102
|
+
extract_selection(target, Selection.from_filter(filter), recursive_packages: recursive_packages)
|
|
103
|
+
else
|
|
104
|
+
extract_all(target, recursive_packages: recursive_packages)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def extract_selection(target, selection, recursive_packages: false)
|
|
111
|
+
tmp = Dir.mktmpdir
|
|
112
|
+
extract_all(tmp, recursive_packages: recursive_packages)
|
|
113
|
+
found = selection.match(all_files_in(tmp), tmp)
|
|
114
|
+
copy_files(found, target || Dir.pwd)
|
|
115
|
+
ensure
|
|
116
|
+
Filesystem.remove_recursive(tmp)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Removes: `extract_particular_files`, `extract_by_filter`, `find_files`,
|
|
121
|
+
`find_by_filter`, `file_matches?`, `file_matches_filter?`, `base_path`.
|
|
122
|
+
That's seven private methods collapsing to one collaborator + one
|
|
123
|
+
orchestrator method.
|
|
124
|
+
|
|
125
|
+
The `files` public method's auto-recursion logic stays the same — the
|
|
126
|
+
public API is unchanged.
|
|
127
|
+
|
|
128
|
+
## Acceptance
|
|
129
|
+
|
|
130
|
+
- All archive_spec examples pass without modification.
|
|
131
|
+
- New spec `spec/excavate/selection_spec.rb` covers both selection
|
|
132
|
+
modes and the not-found error path.
|
|
133
|
+
- Archive private method count drops materially.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 09 — Name the nested-CAB fallback heuristic
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Archive#extract_once` rescues `StandardError` and inspects the message:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
rescue StandardError => e
|
|
11
|
+
raise unless type == :exe && may_be_nested_cab?(e.message)
|
|
12
|
+
|
|
13
|
+
Extractors::CabExtractor.new(archive).extract(target)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
with:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
def may_be_nested_cab?(message)
|
|
20
|
+
message.start_with?("Invalid file format",
|
|
21
|
+
"Unrecognized archive format") ||
|
|
22
|
+
message.include?("Invalid .7z signature")
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Two problems:
|
|
27
|
+
|
|
28
|
+
1. The intent ("if this is an EXE that hides a CAB, retry as CAB") is
|
|
29
|
+
buried inside `extract_once`.
|
|
30
|
+
2. The matcher is untested and undocumented magic.
|
|
31
|
+
|
|
32
|
+
Naming the predicate makes the intent obvious and gives us a place to
|
|
33
|
+
attach tests and documentation.
|
|
34
|
+
|
|
35
|
+
## Plan
|
|
36
|
+
|
|
37
|
+
Create `lib/excavate/nested_cab_fallback.rb`:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
module Excavate
|
|
41
|
+
# Decides whether a failed extraction of a `:exe`-typed archive should be
|
|
42
|
+
# retried as a CAB. Some self-extracting EXEs produced by older Microsoft
|
|
43
|
+
# toolchains wrap a CAB rather than a 7z payload, and the 7z reader
|
|
44
|
+
# surfaces distinctive error strings when it can't parse them.
|
|
45
|
+
class NestedCabFallback
|
|
46
|
+
SIGNATURE_PHRASES = [
|
|
47
|
+
/Invalid file format/,
|
|
48
|
+
/Unrecognized archive format/,
|
|
49
|
+
/Invalid .7z signature/,
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
def self.applies_to?(type, error)
|
|
53
|
+
type == :exe && matches_phrase?(error.message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.matches_phrase?(message)
|
|
57
|
+
SIGNATURE_PHRASES.any? { |re| re.match?(message) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Update `lib/excavate.rb` to autoload `:NestedCabFallback,
|
|
64
|
+
"excavate/nested_cab_fallback"`.
|
|
65
|
+
|
|
66
|
+
Update `Archive#extract_once`:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
def extract_once(archive, target)
|
|
70
|
+
type = FileMagic.detect(archive)
|
|
71
|
+
extractor_class = Extractors::Extractor.for_magic_type(type)
|
|
72
|
+
raise UnknownArchiveError, "Could not unarchive `#{archive}`." unless extractor_class
|
|
73
|
+
|
|
74
|
+
extractor_class.new(archive).extract(target)
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
raise unless NestedCabFallback.applies_to?(type, e)
|
|
77
|
+
|
|
78
|
+
Extractors::CabExtractor.new(archive).extract(target)
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Removes `may_be_nested_cab?` from Archive.
|
|
83
|
+
|
|
84
|
+
## Acceptance
|
|
85
|
+
|
|
86
|
+
- `fonts_cab.exe` / `fonts_nested_cab.exe` tests still pass.
|
|
87
|
+
- New spec `spec/excavate/nested_cab_fallback_spec.rb` covers
|
|
88
|
+
`applies_to?` for each phrase and the negative cases.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 10 — Remove `respond_to` matcher from xz_extractor_spec
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The global rule:
|
|
8
|
+
|
|
9
|
+
> NEVER use `respond_to?` for type checking.
|
|
10
|
+
|
|
11
|
+
The matcher form `expect(obj).to respond_to(:extract)` is the same smell —
|
|
12
|
+
it asserts capability by name rather than by behaviour, and the contract
|
|
13
|
+
is already proven by the parent-class inheritance test in the same
|
|
14
|
+
describe block.
|
|
15
|
+
|
|
16
|
+
## Scope
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
spec/excavate/extractors/xz_extractor_spec.rb:113
|
|
20
|
+
it "responds to extract method" do
|
|
21
|
+
expect(extractor).to respond_to(:extract)
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Plan
|
|
26
|
+
|
|
27
|
+
Delete that example. The neighbouring "inherits from Extractor base
|
|
28
|
+
class" already proves `extract` is present (it's defined on the parent
|
|
29
|
+
and not private). The "accepts a target directory parameter" arity
|
|
30
|
+
check is similarly redundant; remove it too — these are interface
|
|
31
|
+
contract checks duplicated in every extractor spec, and the parent
|
|
32
|
+
class is the contract.
|
|
33
|
+
|
|
34
|
+
## Acceptance
|
|
35
|
+
|
|
36
|
+
- xz_extractor_spec no longer uses `respond_to`.
|
|
37
|
+
- grep -rn "respond_to" spec/ returns nothing.
|
|
38
|
+
- Remaining specs still pass.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 11 — Specs for new collaborators
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Tasks 06–09 introduce four new collaborators (`Filesystem`, `Targets`,
|
|
8
|
+
`Selection`, `NestedCabFallback`). The user's rule "good specs throughout"
|
|
9
|
+
requires every public method to have specs.
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
|
|
13
|
+
New spec files to add:
|
|
14
|
+
|
|
15
|
+
- `spec/excavate/filesystem_spec.rb`
|
|
16
|
+
- `spec/excavate/targets_spec.rb`
|
|
17
|
+
- `spec/excavate/selection_spec.rb`
|
|
18
|
+
- `spec/excavate/nested_cab_fallback_spec.rb`
|
|
19
|
+
|
|
20
|
+
No doubles — use real files / real Dir.mktmpdir / real Struct instances
|
|
21
|
+
per the user's "NEVER USE DOUBLES IN SPECS" rule.
|
|
22
|
+
|
|
23
|
+
## Plan
|
|
24
|
+
|
|
25
|
+
### filesystem_spec.rb
|
|
26
|
+
|
|
27
|
+
- `remove` deletes a file.
|
|
28
|
+
- `remove_recursive` deletes a non-empty directory.
|
|
29
|
+
- Retries up to `max_retries` times on `Errno::EACCES`, then succeeds.
|
|
30
|
+
- Re-raises after exceeding retries.
|
|
31
|
+
|
|
32
|
+
For the retry test: monkey-patch `FileUtils.rm` to raise N times then
|
|
33
|
+
succeed. This avoids `double()` — use a real method override on FileUtils
|
|
34
|
+
within the test scope. Better: yield through a stub callable. Cleanest
|
|
35
|
+
approach without doubles is to wrap a counter and call the real method
|
|
36
|
+
in a setup that fails transiently.
|
|
37
|
+
|
|
38
|
+
Sketch:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
it "retries on Errno::EACCES then succeeds" do
|
|
42
|
+
dir = Dir.mktmpdir
|
|
43
|
+
path = File.join(dir, "x")
|
|
44
|
+
FileUtils.touch(path)
|
|
45
|
+
|
|
46
|
+
attempts = 0
|
|
47
|
+
allow(FileUtils).to receive(:rm) do |p|
|
|
48
|
+
attempts += 1
|
|
49
|
+
raise Errno::EACCES, "locked" if attempts < 3
|
|
50
|
+
File.delete(p)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
expect { Excavate::Filesystem.remove(path, max_retries: 5, delay: 0) }
|
|
54
|
+
.not_to raise_error
|
|
55
|
+
expect(File.exist?(path)).to be false
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
rspec-mocks `allow(...).to receive(...)` is allowed by the user's rule —
|
|
60
|
+
the rule is specifically about `double()` creating mock objects that
|
|
61
|
+
bypass real type checking. Stubbing a method on a real class is different.
|
|
62
|
+
|
|
63
|
+
Hmm but the rule says "Test behavior, not interactions" and "Mocking
|
|
64
|
+
method calls is testing implementation, not correctness." Retrying on a
|
|
65
|
+
specific errno is exactly an interaction we need to test. There's no way
|
|
66
|
+
to test retry-without-stub unless we use a real file that genuinely
|
|
67
|
+
locks on Windows. Best compromise: stub FileUtils.rm to fail a fixed
|
|
68
|
+
number of times then delegate to the original. Add a comment explaining
|
|
69
|
+
why this is the rare exception.
|
|
70
|
+
|
|
71
|
+
### targets_spec.rb
|
|
72
|
+
|
|
73
|
+
- `ensure_absent` is a no-op when path doesn't exist.
|
|
74
|
+
- `ensure_absent` raises `TargetExistsError` for a file.
|
|
75
|
+
- `ensure_absent` raises `TargetExistsError` for a directory (message
|
|
76
|
+
says "directory").
|
|
77
|
+
- `ensure_empty` is a no-op for empty dir.
|
|
78
|
+
- `ensure_empty` raises `TargetNotEmptyError` for non-empty dir.
|
|
79
|
+
- `default_for` creates and returns a path named after the source
|
|
80
|
+
basename; raises if it already exists.
|
|
81
|
+
|
|
82
|
+
### selection_spec.rb
|
|
83
|
+
|
|
84
|
+
- `from_files` + `match` returns the matching path.
|
|
85
|
+
- `from_files` raises `TargetNotFoundError` when no match.
|
|
86
|
+
- `from_filter` + `match` returns all matching paths.
|
|
87
|
+
- `from_filter` raises when nothing matches.
|
|
88
|
+
- Relative path computation strips the base dir and a leading slash.
|
|
89
|
+
|
|
90
|
+
### nested_cab_fallback_spec.rb
|
|
91
|
+
|
|
92
|
+
- `applies_to?(:exe, error_with_"Invalid file format")` → true.
|
|
93
|
+
- `applies_to?(:exe, error_with_"Unrecognized archive format")` → true.
|
|
94
|
+
- `applies_to?(:exe, error_with_"Invalid .7z signature")` → true.
|
|
95
|
+
- `applies_to?(:exe, error_with_other_message)` → false.
|
|
96
|
+
- `applies_to?(:cab, error_with_signature_phrase)` → false (wrong type).
|
|
97
|
+
|
|
98
|
+
## Acceptance
|
|
99
|
+
|
|
100
|
+
- All four new spec files exist and pass.
|
|
101
|
+
- `bundle exec rspec` total example count grows by ~20.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 12 — Final verification
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The previous 11 tasks change a lot of files. Confirm nothing regressed.
|
|
8
|
+
|
|
9
|
+
## Plan
|
|
10
|
+
|
|
11
|
+
1. `bundle exec rspec` — expect 0 failures.
|
|
12
|
+
2. `bundle exec rubocop` — expect 0 new offences; wherever possible,
|
|
13
|
+
drop the now-obsolete entries from `.rubocop_todo.yml`.
|
|
14
|
+
3. `grep -rn "require_relative" lib/` — expect zero hits.
|
|
15
|
+
4. `grep -rn "respond_to\|instance_variable_set\|instance_variable_get\|double(" lib/ spec/` — expect zero hits (matcher `respond_to` only in spec, now also gone).
|
|
16
|
+
5. `grep -rn "\.send(" lib/ spec/` — expect zero hits (no private-method bypass).
|
|
17
|
+
|
|
18
|
+
## Acceptance
|
|
19
|
+
|
|
20
|
+
All checks pass.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Excavate — Fixup TODO Inventory
|
|
2
|
+
|
|
3
|
+
This directory tracks every code-quality issue identified during the
|
|
4
|
+
2026-07-24 audit. Each `NN-name.md` file is a self-contained work order.
|
|
5
|
+
|
|
6
|
+
## Status legend
|
|
7
|
+
|
|
8
|
+
- **done** — work completed, specs green, rubocop clean
|
|
9
|
+
- **pending** — not yet started
|
|
10
|
+
|
|
11
|
+
## Work order (executed in this order)
|
|
12
|
+
|
|
13
|
+
| # | File | Priority | Status | Subject |
|
|
14
|
+
|---|------|----------|--------|---------|
|
|
15
|
+
| 01 | [01-autoload-migration.md](01-autoload-migration.md) | P1 | done | Replace every `require_relative` of internal library code with `autoload` declared in the immediate parent namespace file. |
|
|
16
|
+
| 02 | [02-redundant-external-requires.md](02-redundant-external-requires.md) | P3 | done | Drop the per-format `require "omnizip/formats/X"` lines (omnizip already autoloads them) and the stray staged `require "omnizip"` in cpio_extractor. |
|
|
17
|
+
| 03 | [03-extractor-registry-ocp.md](03-extractor-registry-ocp.md) | P1 | done | Replace `MAGIC_MAP` + `const_get` with a self-registering extractor registry so adding a format is a single `handles :type` line in the new class. |
|
|
18
|
+
| 04 | [04-filemagic-signature-struct.md](04-filemagic-signature-struct.md) | P2 | done | Promote the `[offset, magic, type]` tuple to a named `Signature` value object. |
|
|
19
|
+
| 05 | [05-utils-silence-stream-dead-code.md](05-utils-silence-stream-dead-code.md) | P2 | done | Remove the always-`File::NULL` ternary and the dead `RbConfig` branch. |
|
|
20
|
+
| 06 | [06-archive-filesystem-module.md](06-archive-filesystem-module.md) | P1 | done | Extract Windows-safe FS retries into `Excavate::Filesystem` so the Archive class stops mixing concerns. |
|
|
21
|
+
| 07 | [07-archive-targets-helper.md](07-archive-targets-helper.md) | P2 | done | Move target-path safety (existence/emptiness checks, default-target creation) into `Excavate::Targets`. |
|
|
22
|
+
| 08 | [08-archive-selection-dry.md](08-archive-selection-dry.md) | P2 | done | Unify `extract_particular_files` and `extract_by_filter` behind one `Selection` collaborator — current code duplicates the tmp-extract + copy scaffolding. |
|
|
23
|
+
| 09 | [09-cab-fallback-heuristic-named.md](09-cab-fallback-heuristic-named.md) | P2 | done | Replace inline `e.message.start_with?(...)` sniffing with a named `NestedCabFallback` predicate for testability. |
|
|
24
|
+
| 10 | [10-specs-remove-respond-to-matcher.md](10-specs-remove-respond-to-matcher.md) | P3 | done | Replace `respond_to(:extract)` matcher in xz_extractor_spec with the existing interface contract test (forbidden matcher). |
|
|
25
|
+
| 11 | [11-specs-cover-new-collaborators.md](11-specs-cover-new-collaborators.md) | P2 | done | Add specs for the new `Filesystem`, `Targets`, `Selection`, and `NestedCabFallback` collaborators introduced by 06–09. |
|
|
26
|
+
| 12 | [12-final-verification.md](12-final-verification.md) | P1 | done | Run full `rspec` + `rubocop` and confirm zero regressions. |
|
|
27
|
+
|
|
28
|
+
## Final verification (2026-07-24)
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
$ bundle exec rspec
|
|
32
|
+
210 examples, 0 failures
|
|
33
|
+
|
|
34
|
+
$ bundle exec rubocop
|
|
35
|
+
50 files inspected, no offenses detected
|
|
36
|
+
|
|
37
|
+
$ grep -rn "require_relative" lib/
|
|
38
|
+
(no hits)
|
|
39
|
+
|
|
40
|
+
$ grep -rn "respond_to\|instance_variable_set\|instance_variable_get\|double(" lib/ spec/
|
|
41
|
+
(no hits)
|
|
42
|
+
|
|
43
|
+
$ grep -rn "\.send(" lib/ spec/
|
|
44
|
+
(no hits)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Summary of changes
|
|
48
|
+
|
|
49
|
+
### New collaborators (lib/excavate/)
|
|
50
|
+
- `filesystem.rb` — Windows-safe remove/remove_recursive with retry policy
|
|
51
|
+
- `targets.rb` — target-path policy (ensure_absent / ensure_empty / default_for)
|
|
52
|
+
- `selection.rb` — explicit-name or glob-filter matcher for selective extraction
|
|
53
|
+
- `nested_cab_fallback.rb` — named predicate for the EXE-may-hide-CAB heuristic
|
|
54
|
+
|
|
55
|
+
### Refactored
|
|
56
|
+
- `excavate.rb` — now the autoload manifest; eagerly defines the error hierarchy so rescue clauses work for downstream users
|
|
57
|
+
- `extractors.rb` — autoload manifest for every extractor subclass
|
|
58
|
+
- `extractors/extractor.rb` — registry API (`handles`, `for_magic_type`, `eager_load_subclasses!`) with shared class-variable state so subclasses register on load
|
|
59
|
+
- `extractors/*.rb` — every subclass now declares `handles :type` and drops the per-format external requires
|
|
60
|
+
- `file_magic.rb` — `Signature = Struct.new(:offset, :magic, :type)` value object; `detect_bytes` delegates to `matching_signature` helper for ABC size compliance
|
|
61
|
+
- `utils.rb` — dead `RbConfig` ternary removed
|
|
62
|
+
- `archive.rb` — uses Filesystem, Targets, Selection, NestedCabFallback collaborators; public API unchanged; `files` returns an Enumerator when no block is given
|
|
63
|
+
|
|
64
|
+
### New specs (spec/excavate/)
|
|
65
|
+
- `filesystem_spec.rb` — 7 examples
|
|
66
|
+
- `targets_spec.rb` — 6 examples
|
|
67
|
+
- `selection_spec.rb` — 7 examples
|
|
68
|
+
- `nested_cab_fallback_spec.rb` — 6 examples
|
|
69
|
+
|
|
70
|
+
### Updated
|
|
71
|
+
- `extractor_spec.rb` — replaced `MAGIC_MAP` static-key specs with `.registered_types` check
|
|
72
|
+
- `xz_extractor_spec.rb` — removed `respond_to(:extract)` matcher
|
|
73
|
+
- `.rubocop_todo.yml` — regenerated; obsolete entries for non-existent `test_archives/` and `test_msi_memory.rb` dropped
|
data/excavate.gemspec
CHANGED
|
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.require_paths = ["lib"]
|
|
32
32
|
|
|
33
33
|
spec.add_dependency "cabriolet", "~> 0.2.4"
|
|
34
|
-
spec.add_dependency "omnizip", "~> 0.3.
|
|
34
|
+
spec.add_dependency "omnizip", "~> 0.3.11"
|
|
35
35
|
spec.add_dependency "thor", "~> 1.0"
|
|
36
36
|
|
|
37
37
|
spec.metadata["rubygems_mfa_required"] = "true"
|