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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8c1a236bccd731f1a145805e3fe1288bb9536493331657ff0718e17be9f847b
|
|
4
|
+
data.tar.gz: c60465a7191a66dd237d4ae88e6dc631cd26c6be1e1f161f594358e4d1adcdce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dda5395e5512553bd35c793c186042d5b64b5a72cec221d30dfad325966e88af3364c1470f7d2afbfa595e68dee90219ac0ce4279ac7301fb4efc400498ef1a8
|
|
7
|
+
data.tar.gz: 979eb4fe6b6af40579c9b80b77e92f5229117deca264fa2035a0cdd1dd5d28ece49d0c2082ee7fc7578439826424522c30c669e70f2f8ea468ed5ded88dce3b8
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-
|
|
3
|
+
# on 2026-07-24 04:23:45 UTC using RuboCop version 1.85.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,7 +11,7 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'excavate.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 6
|
|
15
15
|
# This cop supports safe autocorrection (--autocorrect).
|
|
16
16
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
17
17
|
# URISchemes: http, https
|
|
@@ -20,77 +20,12 @@ Layout/LineLength:
|
|
|
20
20
|
- 'bin/rspec'
|
|
21
21
|
- 'excavate.gemspec'
|
|
22
22
|
- 'lib/excavate/extractors/rpm_extractor.rb'
|
|
23
|
-
- 'lib/excavate/utils.rb'
|
|
24
23
|
- 'spec/excavate/archive_spec.rb'
|
|
25
|
-
- 'test_archives/memory_test.rb'
|
|
26
|
-
- 'test_msi_memory.rb'
|
|
27
24
|
|
|
28
|
-
# Offense count:
|
|
29
|
-
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
|
30
|
-
Lint/DuplicateBranch:
|
|
31
|
-
Exclude:
|
|
32
|
-
- 'lib/excavate/utils.rb'
|
|
33
|
-
|
|
34
|
-
# Offense count: 2
|
|
35
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
36
|
-
Lint/ScriptPermission:
|
|
37
|
-
Exclude:
|
|
38
|
-
- 'test_archives/memory_test.rb'
|
|
39
|
-
- 'test_msi_memory.rb'
|
|
40
|
-
|
|
41
|
-
# Offense count: 1
|
|
42
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
43
|
-
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
|
44
|
-
Lint/UnusedBlockArgument:
|
|
45
|
-
Exclude:
|
|
46
|
-
- 'test_msi_memory.rb'
|
|
47
|
-
|
|
48
|
-
# Offense count: 4
|
|
49
|
-
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
50
|
-
Metrics/AbcSize:
|
|
51
|
-
Exclude:
|
|
52
|
-
- 'test_archives/memory_test.rb'
|
|
53
|
-
- 'test_msi_memory.rb'
|
|
54
|
-
|
|
55
|
-
# Offense count: 2
|
|
56
|
-
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
57
|
-
Metrics/CyclomaticComplexity:
|
|
58
|
-
Exclude:
|
|
59
|
-
- 'test_archives/memory_test.rb'
|
|
60
|
-
- 'test_msi_memory.rb'
|
|
61
|
-
|
|
62
|
-
# Offense count: 11
|
|
25
|
+
# Offense count: 5
|
|
63
26
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
64
27
|
Metrics/MethodLength:
|
|
65
|
-
Max:
|
|
66
|
-
|
|
67
|
-
# Offense count: 2
|
|
68
|
-
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
69
|
-
Metrics/PerceivedComplexity:
|
|
70
|
-
Exclude:
|
|
71
|
-
- 'test_archives/memory_test.rb'
|
|
72
|
-
- 'test_msi_memory.rb'
|
|
73
|
-
|
|
74
|
-
# Offense count: 2
|
|
75
|
-
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
|
|
76
|
-
# AllowedMethods: call
|
|
77
|
-
# WaywardPredicates: infinite?, nonzero?
|
|
78
|
-
Naming/PredicateMethod:
|
|
79
|
-
Exclude:
|
|
80
|
-
- 'test_archives/memory_test.rb'
|
|
81
|
-
- 'test_msi_memory.rb'
|
|
82
|
-
|
|
83
|
-
# Offense count: 1
|
|
84
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
85
|
-
Performance/RegexpMatch:
|
|
86
|
-
Exclude:
|
|
87
|
-
- 'test_archives/memory_test.rb'
|
|
88
|
-
|
|
89
|
-
# Offense count: 1
|
|
90
|
-
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
91
|
-
Performance/StringInclude:
|
|
92
|
-
Exclude:
|
|
93
|
-
- 'test_archives/memory_test.rb'
|
|
28
|
+
Max: 11
|
|
94
29
|
|
|
95
30
|
# Offense count: 46
|
|
96
31
|
# Configuration parameters: Prefixes, AllowedPatterns.
|
|
@@ -101,12 +36,12 @@ RSpec/ContextWording:
|
|
|
101
36
|
- 'spec/excavate/cli_spec.rb'
|
|
102
37
|
- 'spec/support/fresh_work_dir.rb'
|
|
103
38
|
|
|
104
|
-
# Offense count:
|
|
39
|
+
# Offense count: 18
|
|
105
40
|
# Configuration parameters: CountAsOne.
|
|
106
41
|
RSpec/ExampleLength:
|
|
107
42
|
Max: 16
|
|
108
43
|
|
|
109
|
-
# Offense count:
|
|
44
|
+
# Offense count: 32
|
|
110
45
|
RSpec/MultipleExpectations:
|
|
111
46
|
Max: 2
|
|
112
47
|
|
|
@@ -114,26 +49,3 @@ RSpec/MultipleExpectations:
|
|
|
114
49
|
# Configuration parameters: AllowedGroups.
|
|
115
50
|
RSpec/NestedGroups:
|
|
116
51
|
Max: 4
|
|
117
|
-
|
|
118
|
-
# Offense count: 8
|
|
119
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
120
|
-
# Configuration parameters: EnforcedStyle.
|
|
121
|
-
# SupportedStyles: trailing_conditional, ternary
|
|
122
|
-
Style/EmptyStringInsideInterpolation:
|
|
123
|
-
Exclude:
|
|
124
|
-
- 'test_archives/memory_test.rb'
|
|
125
|
-
- 'test_msi_memory.rb'
|
|
126
|
-
|
|
127
|
-
# Offense count: 2
|
|
128
|
-
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
129
|
-
Style/IdenticalConditionalBranches:
|
|
130
|
-
Exclude:
|
|
131
|
-
- 'lib/excavate/utils.rb'
|
|
132
|
-
|
|
133
|
-
# Offense count: 1
|
|
134
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
135
|
-
# Configuration parameters: EnforcedStyle.
|
|
136
|
-
# SupportedStyles: implicit, explicit
|
|
137
|
-
Style/RescueStandardError:
|
|
138
|
-
Exclude:
|
|
139
|
-
- 'test_msi_memory.rb'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# 01 — Autoload migration
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
User's global rule:
|
|
8
|
+
|
|
9
|
+
> NEVER use `require_relative` for internal library code. Never use `require`
|
|
10
|
+
> with a path to code within your own library. Use Ruby `autoload` instead.
|
|
11
|
+
> Define autoload entries in the immediate parent namespace's file — create
|
|
12
|
+
> that file if it doesn't exist.
|
|
13
|
+
|
|
14
|
+
Currently `lib/excavate.rb` and `lib/excavate/extractors.rb` eagerly pull in
|
|
15
|
+
every file with `require_relative`, defeating lazy loading and creating
|
|
16
|
+
implicit load-order couplings.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Offending `require_relative` calls inside `lib/`:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
lib/excavate.rb:3 require_relative "excavate/version"
|
|
24
|
+
lib/excavate.rb:4 require_relative "excavate/extractors"
|
|
25
|
+
lib/excavate.rb:5 require_relative "excavate/archive"
|
|
26
|
+
lib/excavate.rb:6 require_relative "excavate/file_magic"
|
|
27
|
+
lib/excavate.rb:7 require_relative "excavate/utils"
|
|
28
|
+
lib/excavate/cli.rb:3 require_relative "../excavate"
|
|
29
|
+
lib/excavate/extractors.rb:1 require_relative "extractors/extractor"
|
|
30
|
+
lib/excavate/extractors.rb:2 require_relative "extractors/cab_extractor"
|
|
31
|
+
lib/excavate/extractors.rb:3 require_relative "extractors/cpio_extractor"
|
|
32
|
+
lib/excavate/extractors.rb:4 require_relative "extractors/gzip_extractor"
|
|
33
|
+
lib/excavate/extractors.rb:5 require_relative "extractors/ole_extractor"
|
|
34
|
+
lib/excavate/extractors.rb:6 require_relative "extractors/rpm_extractor"
|
|
35
|
+
lib/excavate/extractors.rb:7 require_relative "extractors/seven_zip_extractor"
|
|
36
|
+
lib/excavate/extractors.rb:8 require_relative "extractors/tar_extractor"
|
|
37
|
+
lib/excavate/extractors.rb:9 require_relative "extractors/xar_extractor"
|
|
38
|
+
lib/excavate/extractors.rb:10 require_relative "extractors/xz_extractor"
|
|
39
|
+
lib/excavate/extractors.rb:11 require_relative "extractors/zip_extractor"
|
|
40
|
+
lib/excavate/extractors/ole_extractor.rb:5 require_relative "../file_magic"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Plan
|
|
44
|
+
|
|
45
|
+
1. `lib/excavate.rb` — the immediate-parent file for `Excavate::*`. Move
|
|
46
|
+
error classes to `lib/excavate/errors.rb` and autoload it. Autoload
|
|
47
|
+
`Archive`, `FileMagic`, `Utils`, `CLI`, `Extractors`. Keep
|
|
48
|
+
`Excavate::VERSION` autoloaded too.
|
|
49
|
+
2. `lib/excavate/extractors.rb` — the immediate-parent file for
|
|
50
|
+
`Excavate::Extractors::*`. Replace all 11 `require_relative` lines with
|
|
51
|
+
`autoload` declarations.
|
|
52
|
+
3. `lib/excavate/cli.rb` — drop the `require_relative "../excavate"`. CLI is
|
|
53
|
+
now autoloaded by the parent; the `thor` external require stays.
|
|
54
|
+
4. `lib/excavate/extractors/ole_extractor.rb` — drop the
|
|
55
|
+
`require_relative "../file_magic"`. `FileMagic` is reachable via the
|
|
56
|
+
autoload chain `Excavate → FileMagic`.
|
|
57
|
+
5. Create `lib/excavate/errors.rb` hosting the error hierarchy; have
|
|
58
|
+
`excavate.rb` autoload it eagerly so exception classes are always
|
|
59
|
+
defined for rescue clauses.
|
|
60
|
+
|
|
61
|
+
## Acceptance
|
|
62
|
+
|
|
63
|
+
- `grep -rn "require_relative" lib/` returns zero hits.
|
|
64
|
+
- `bundle exec rspec` is green.
|
|
65
|
+
- Loading the gem remains a single `require "excavate"` (Bundler-friendly).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 02 — Drop redundant external requires
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`omnizip` itself uses `autoload` for every `Formats::*` constant (see
|
|
8
|
+
`omnizip/formats.rb`). The per-format `require "omnizip/formats/cpio"` (and
|
|
9
|
+
siblings) in our extractors are redundant — they re-trigger the same autoload
|
|
10
|
+
resolution.
|
|
11
|
+
|
|
12
|
+
Additionally the currently-staged (uncommitted) change to
|
|
13
|
+
`cpio_extractor.rb` adds a second external require line:
|
|
14
|
+
`require "omnizip"` followed by `require "omnizip/formats/cpio"`. Only one is
|
|
15
|
+
needed.
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
|
|
19
|
+
Files with redundant per-format requires:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
lib/excavate/extractors/cpio_extractor.rb:3-4 require "omnizip" / require "omnizip/formats/cpio"
|
|
23
|
+
lib/excavate/extractors/ole_extractor.rb:3-4 require "omnizip" / require "omnizip/formats/ole"
|
|
24
|
+
lib/excavate/extractors/rpm_extractor.rb:3-4 require "omnizip" / require "omnizip/formats/rpm"
|
|
25
|
+
lib/excavate/extractors/xar_extractor.rb:3-4 require "omnizip" / require "omnizip/formats/xar"
|
|
26
|
+
lib/excavate/extractors/seven_zip_extractor.rb:3 require "omnizip"
|
|
27
|
+
lib/excavate/extractors/tar_extractor.rb:3 require "omnizip"
|
|
28
|
+
lib/excavate/extractors/xz_extractor.rb:3 require "omnizip"
|
|
29
|
+
lib/excavate/extractors/zip_extractor.rb:3 require "omnizip"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Plan
|
|
33
|
+
|
|
34
|
+
Keep one `require "omnizip"` per file that touches an Omnizip constant.
|
|
35
|
+
Delete the per-format `require "omnizip/formats/X"` lines — the autoload in
|
|
36
|
+
`omnizip/formats.rb` resolves them on first reference. The staged
|
|
37
|
+
`require "omnizip"` addition in cpio_extractor.rb becomes the single
|
|
38
|
+
omnizip require for that file (replacing the redundant format require).
|
|
39
|
+
|
|
40
|
+
For extractors that only touch a single Omnizip format constant, the
|
|
41
|
+
`require "omnizip"` line is also technically unneeded if any other loaded
|
|
42
|
+
file already requires omnizip — but keeping it makes each file
|
|
43
|
+
independently loadable, which is good practice. Keep them.
|
|
44
|
+
|
|
45
|
+
## Acceptance
|
|
46
|
+
|
|
47
|
+
- No `require "omnizip/formats/..."` remains in `lib/`.
|
|
48
|
+
- `bundle exec rspec` is green.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 03 — Open/closed Extractor registry
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Excavate::Extractors::Extractor.for_magic_type` currently consults a frozen
|
|
8
|
+
`MAGIC_MAP` keyed by symbol → class-name-string and resolves via
|
|
9
|
+
`const_get`. Adding a new extractor requires editing this hash. That
|
|
10
|
+
violates OCP: the base class is open for *modification*, not just extension.
|
|
11
|
+
|
|
12
|
+
The current spec even hard-codes the expected key list
|
|
13
|
+
(`spec/excavate/extractors/extractor_spec.rb:6`), which means every new
|
|
14
|
+
extractor needs edits in three places: the new file, the parent
|
|
15
|
+
`extractors.rb`, and `MAGIC_MAP`. After this refactor only the new file
|
|
16
|
+
needs to change.
|
|
17
|
+
|
|
18
|
+
## Plan
|
|
19
|
+
|
|
20
|
+
Replace the hash + const_get with a registry pattern:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class Extractor
|
|
24
|
+
@registry = {}
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
private :inherited # keep the hook itself private — registration is the API
|
|
28
|
+
|
|
29
|
+
def inherited(subclass)
|
|
30
|
+
super
|
|
31
|
+
subclass.instance_variable_set(:@handled_types, [])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handles(*types)
|
|
35
|
+
@handled_types.concat(types)
|
|
36
|
+
Extractor.register(self, types)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.register(klass, types)
|
|
41
|
+
types.each { |t| registry[t] = klass }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.for_magic_type(type)
|
|
45
|
+
registry[type]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.registry
|
|
49
|
+
@registry
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Wait — the user forbids `instance_variable_set`. Use a different shape:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
class Extractor
|
|
58
|
+
@registry = {}
|
|
59
|
+
|
|
60
|
+
class << self
|
|
61
|
+
attr_reader :registry
|
|
62
|
+
|
|
63
|
+
def handles(*types)
|
|
64
|
+
types.each { |t| registry[t] = self }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def for_magic_type(type)
|
|
68
|
+
registry[type]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inherited(subclass)
|
|
72
|
+
super
|
|
73
|
+
subclass.instance_variable_set(:@handled_types, []) # FORBIDDEN
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Final design — no instance_variable_* and no const_get. Each subclass calls
|
|
80
|
+
`handles :cab` at class-body load time, which writes itself into the
|
|
81
|
+
parent's registry hash. The `inherited` hook is not needed because
|
|
82
|
+
registration is explicit at class definition.
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# extractors/extractor.rb
|
|
86
|
+
module Excavate
|
|
87
|
+
module Extractors
|
|
88
|
+
class Extractor
|
|
89
|
+
class << self
|
|
90
|
+
# Public registry API. Each subclass declares the magic types it
|
|
91
|
+
# handles via `handles :type, ...` at class-body load time.
|
|
92
|
+
def handles(*magic_types)
|
|
93
|
+
magic_types.each { |t| registry[t] = self }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def for_magic_type(type)
|
|
97
|
+
registry[type]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def registry
|
|
101
|
+
@registry ||= {}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def initialize(archive)
|
|
106
|
+
@archive = archive
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def extract(_target)
|
|
110
|
+
raise NotImplementedError, "you must implement #extract"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def extract_inner(data, target) ... end
|
|
116
|
+
def write_raw_output(data, target) ... end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each subclass:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class CabExtractor < Extractor
|
|
126
|
+
handles :cab
|
|
127
|
+
...
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
class SevenZipExtractor < Extractor
|
|
131
|
+
handles :seven_zip, :exe
|
|
132
|
+
...
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Spec impact
|
|
137
|
+
|
|
138
|
+
- Remove `MAGIC_MAP` frozen spec — the registry hash is no longer public.
|
|
139
|
+
- Replace the per-type expectation loop with a "registered" check using
|
|
140
|
+
`Extractor.for_magic_type(...)`.
|
|
141
|
+
- The "is registered for :type" specs in each extractor_spec.rb still pass
|
|
142
|
+
unchanged because they assert via `for_magic_type`.
|
|
143
|
+
|
|
144
|
+
## Acceptance
|
|
145
|
+
|
|
146
|
+
- Adding a new format requires only: new file under
|
|
147
|
+
`lib/excavate/extractors/`, new autoload entry in
|
|
148
|
+
`lib/excavate/extractors.rb`, `handles :foo` in the class body.
|
|
149
|
+
- No edits to any other file.
|
|
150
|
+
- All 185 specs pass (after extractor_spec is updated to drop MAGIC_MAP).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 04 — FileMagic::Signature value object
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`FileMagic::SIGNATURES` is an array of `[offset, magic, type]` tuples.
|
|
8
|
+
Reading call sites like `SIGNATURES.map { |o, m, _| o + m.bytesize }` or
|
|
9
|
+
`SIGNATURES.each do |offset, magic, type|` requires the reader to memorise
|
|
10
|
+
the positional convention. A named value object removes that cognitive load
|
|
11
|
+
and documents intent.
|
|
12
|
+
|
|
13
|
+
## Plan
|
|
14
|
+
|
|
15
|
+
Introduce `FileMagic::Signature = Struct.new(:offset, :magic, :type)` and
|
|
16
|
+
rewrite SIGNATURES as a list of `Signature.new(...)` literals. Update
|
|
17
|
+
`detect_bytes` and `MAX_READ` to use field names.
|
|
18
|
+
|
|
19
|
+
Sketch:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
module Excavate
|
|
23
|
+
class FileMagic
|
|
24
|
+
Signature = Struct.new(:offset, :magic, :type)
|
|
25
|
+
|
|
26
|
+
SIGNATURES = [
|
|
27
|
+
Signature.new(0, "MSCF\x00\x00\x00\x00".b, :cab),
|
|
28
|
+
Signature.new(0, "\xFD7zXZ\x00".b, :xz),
|
|
29
|
+
Signature.new(0, "\x1F\x8B".b, :gzip),
|
|
30
|
+
Signature.new(257, "ustar".b, :tar),
|
|
31
|
+
Signature.new(0, "7z\xBC\xAF\x27\x1C".b, :seven_zip),
|
|
32
|
+
Signature.new(0, "PK\x03\x04".b, :zip),
|
|
33
|
+
Signature.new(0, "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1".b, :ole),
|
|
34
|
+
Signature.new(0, "xar!".b, :xar),
|
|
35
|
+
Signature.new(0, "\xED\xAB\xEE\xDB".b, :rpm),
|
|
36
|
+
Signature.new(0, "070707".b, :cpio),
|
|
37
|
+
Signature.new(0, "070701".b, :cpio),
|
|
38
|
+
Signature.new(0, "070702".b, :cpio),
|
|
39
|
+
Signature.new(0, "MZ".b, :exe),
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
MAX_READ = SIGNATURES.map { |s| s.offset + s.magic.bytesize }.max
|
|
43
|
+
|
|
44
|
+
def self.detect(path)
|
|
45
|
+
detect_bytes(File.read(path, MAX_READ, mode: "rb"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.detect_bytes(data)
|
|
49
|
+
return nil if data.nil? || data.empty?
|
|
50
|
+
|
|
51
|
+
SIGNATURES.each do |signature|
|
|
52
|
+
next if data.bytesize < signature.offset + signature.magic.bytesize
|
|
53
|
+
|
|
54
|
+
return signature.type if data.byteslice(signature.offset, signature.magic.bytesize) == signature.magic
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`Signature` is internal to FileMagic so no autoload entry needed.
|
|
64
|
+
|
|
65
|
+
## Acceptance
|
|
66
|
+
|
|
67
|
+
- All file_magic_spec examples pass unchanged.
|
|
68
|
+
- No positional-destructuring of signature tuples remains.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# 05 — Clean up `Utils.silence_stream` dead branch
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Current implementation:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def silence_stream(stream)
|
|
11
|
+
old_stream = stream.dup
|
|
12
|
+
stream.reopen(/mswin|mingw/.match?(RbConfig::CONFIG["host_os"]) ? File::NULL : File::NULL)
|
|
13
|
+
stream.sync = true
|
|
14
|
+
yield
|
|
15
|
+
ensure
|
|
16
|
+
stream.reopen(old_stream)
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Both arms of the ternary return `File::NULL`, so the OS-detection branch
|
|
21
|
+
is dead code. RuboCop already flags it as `Lint/DuplicateBranch` and
|
|
22
|
+
`Style/IdenticalConditionalBranches` in `.rubocop_todo.yml`.
|
|
23
|
+
|
|
24
|
+
Also: `RbConfig` is referenced without `require "rbconfig"`. It happens to
|
|
25
|
+
work because Ruby preloads it, but explicit is better.
|
|
26
|
+
|
|
27
|
+
## Plan
|
|
28
|
+
|
|
29
|
+
Two acceptable resolutions:
|
|
30
|
+
|
|
31
|
+
**Option A** — delete the ternary entirely:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
def silence_stream(stream)
|
|
35
|
+
old_stream = stream.dup
|
|
36
|
+
stream.reopen(File::NULL)
|
|
37
|
+
stream.sync = true
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
stream.reopen(old_stream)
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Option B** — delete the whole `Utils` module. Its only caller is
|
|
45
|
+
`spec/excavate/cli_spec.rb` which uses it to swallow `$stdout` during CLI
|
|
46
|
+
tests. That could be replaced with RSpec's `output(...).to_stdout` matcher
|
|
47
|
+
(which is already used elsewhere in the same file) — but the silence_stream
|
|
48
|
+
approach is more robust when the test doesn't care about the message body.
|
|
49
|
+
|
|
50
|
+
Choose **Option A** — keep the helper, just remove the dead code.
|
|
51
|
+
|
|
52
|
+
## Acceptance
|
|
53
|
+
|
|
54
|
+
- `Utils.silence_stream` no longer has identical branches.
|
|
55
|
+
- `Lint/DuplicateBranch` and `Style/IdenticalConditionalBranches` can be
|
|
56
|
+
removed from `.rubocop_todo.yml`.
|
|
57
|
+
- cli_spec continues to silence $stdout.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 06 — Extract `Excavate::Filesystem` helper
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Excavate::Archive` currently owns four Windows-safe retry helpers
|
|
8
|
+
(`windows_safe_rm`, `windows_safe_rm_rf`, plus the inline
|
|
9
|
+
`replace_archive_with_contents` rescue logic). These are pure
|
|
10
|
+
filesystem operations with retry-on-`EACCES`/`ENOTEMPTY` semantics and
|
|
11
|
+
have nothing to do with archives. They violate MECE: the Archive class
|
|
12
|
+
is mixing archive orchestration with low-level FS reliability.
|
|
13
|
+
|
|
14
|
+
## Plan
|
|
15
|
+
|
|
16
|
+
Create `lib/excavate/filesystem.rb`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
module Excavate
|
|
20
|
+
module Filesystem
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
RETRYABLE_ERRORS = [Errno::EACCES, Errno::ENOTEMPTY].freeze
|
|
24
|
+
DEFAULT_MAX_RETRIES = 5
|
|
25
|
+
DEFAULT_RETRY_DELAY = 0.2
|
|
26
|
+
|
|
27
|
+
def remove(path, max_retries: DEFAULT_MAX_RETRIES, delay: DEFAULT_RETRY_DELAY)
|
|
28
|
+
with_retry(max_retries: max_retries, delay: delay) { FileUtils.rm(path) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def remove_recursive(path, max_retries: DEFAULT_MAX_RETRIES, delay: DEFAULT_RETRY_DELAY)
|
|
32
|
+
with_retry(max_retries: max_retries, delay: delay) { FileUtils.rm_rf(path) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_retry(max_retries: DEFAULT_MAX_RETRIES, delay: DEFAULT_RETRY_DELAY)
|
|
36
|
+
attempts = 0
|
|
37
|
+
begin
|
|
38
|
+
yield
|
|
39
|
+
rescue *RETRYABLE_ERRORS => e
|
|
40
|
+
attempts += 1
|
|
41
|
+
raise e if attempts >= max_retries
|
|
42
|
+
|
|
43
|
+
sleep(delay)
|
|
44
|
+
retry
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Update `lib/excavate.rb` to autoload `:Filesystem,
|
|
52
|
+
"excavate/filesystem"`.
|
|
53
|
+
|
|
54
|
+
Update `lib/excavate/archive.rb`:
|
|
55
|
+
|
|
56
|
+
- `windows_safe_rm(path, ...)` → `Filesystem.remove(path)`
|
|
57
|
+
- `windows_safe_rm_rf(path, ...)` → `Filesystem.remove_recursive(path)`
|
|
58
|
+
- The `ensure ... FileUtils.rm_rf(target)` clauses in
|
|
59
|
+
`extract_particular_files` and `extract_by_filter` should also use
|
|
60
|
+
`Filesystem.remove_recursive` for consistency.
|
|
61
|
+
- `replace_archive_with_contents` already rescues Errno::EACCES for a
|
|
62
|
+
*different* reason (Windows file lock during mv); leave it but route
|
|
63
|
+
the `windows_safe_rm(archive)` call through `Filesystem.remove`.
|
|
64
|
+
|
|
65
|
+
## Acceptance
|
|
66
|
+
|
|
67
|
+
- Archive no longer has private `windows_safe_*` methods.
|
|
68
|
+
- New file `lib/excavate/filesystem.rb` exists with autoload.
|
|
69
|
+
- All archive specs still pass.
|
|
70
|
+
- New spec `spec/excavate/filesystem_spec.rb` covers both methods'
|
|
71
|
+
happy path and retry behaviour (use a fake error class with a stub
|
|
72
|
+
that fails N-1 times then succeeds).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 07 — Extract `Excavate::Targets` helper
|
|
2
|
+
|
|
3
|
+
Status: **pending**
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
`Excavate::Archive` owns three target-path safety helpers
|
|
8
|
+
(`ensure_not_exist`, `ensure_empty`, `default_target`). They are pure
|
|
9
|
+
path policy with no archive context — they belong in a focused value
|
|
10
|
+
object so they can be tested in isolation and reused.
|
|
11
|
+
|
|
12
|
+
## Plan
|
|
13
|
+
|
|
14
|
+
Create `lib/excavate/targets.rb`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
module Excavate
|
|
18
|
+
module Targets
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def ensure_absent(path)
|
|
22
|
+
return unless File.exist?(path)
|
|
23
|
+
|
|
24
|
+
kind = File.directory?(path) ? "directory" : "file"
|
|
25
|
+
raise TargetExistsError,
|
|
26
|
+
"Target #{kind} `#{File.basename(path)}` already exists."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ensure_empty(path)
|
|
30
|
+
return if Dir.empty?(path)
|
|
31
|
+
|
|
32
|
+
raise TargetNotEmptyError,
|
|
33
|
+
"Target directory `#{File.basename(path)}` is not empty."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def default_for(source)
|
|
37
|
+
target = File.expand_path(File.basename(source, ".*"))
|
|
38
|
+
ensure_absent(target)
|
|
39
|
+
FileUtils.mkdir(target)
|
|
40
|
+
target
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Update `lib/excavate.rb` to autoload `:Targets, "excavate/targets"`.
|
|
47
|
+
|
|
48
|
+
Update `lib/excavate/archive.rb`:
|
|
49
|
+
|
|
50
|
+
- `ensure_not_exist(path)` → `Targets.ensure_absent(path)`
|
|
51
|
+
- `ensure_empty(path)` → `Targets.ensure_empty(path)`
|
|
52
|
+
- `default_target(source)` → `Targets.default_for(source)`
|
|
53
|
+
|
|
54
|
+
Remove the now-empty private helpers.
|
|
55
|
+
|
|
56
|
+
## Acceptance
|
|
57
|
+
|
|
58
|
+
- Archive no longer has private target-safety methods.
|
|
59
|
+
- New file `lib/excavate/targets.rb` with autoload.
|
|
60
|
+
- New spec `spec/excavate/targets_spec.rb` covers all three methods
|
|
61
|
+
including the error paths.
|
|
62
|
+
- Existing archive + cli specs still pass.
|