wildling 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +76 -0
- data/bin/wildling +29 -0
- data/bin/wildling.rb +9 -0
- data/help.txt +43 -0
- data/lib/wildling/cli.rb +245 -0
- data/lib/wildling/generator.rb +36 -0
- data/lib/wildling/parse_pattern.rb +157 -0
- data/lib/wildling/token.rb +58 -0
- data/lib/wildling/wildling.rb +57 -0
- data/lib/wildling.rb +7 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 17473f92bca01e0d3a1d43266416823b2464bec246c23533db1f7e316a1b527b
|
|
4
|
+
data.tar.gz: 9b5856610a2df75531421c54edc2251bf468987a9c0023caf8d03603318e0781
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d28a9bed8d486cb9386090cd6172e0de961ba57be9195ab24964a72cba41fbff3b4fdd2ed1b96ab11c74b1b977df133ac4ad6c7192eefc6b5edd194b40f20c0c
|
|
7
|
+
data.tar.gz: 4db4fb08a085f3810888f4c43608265a7596fadc0af4cf93dbaf695c94236110960ef474217eca981d70c675c0c1c3a68a202671359bc91e28763796fecd24ce
|
data/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# wildling
|
|
2
|
+
|
|
3
|
+
Ruby library and CLI for pattern-based string generation. **Zero third-party gems** (stdlib only, including `json`). Requires Ruby 3.0+.
|
|
4
|
+
|
|
5
|
+
<!-- wildling:preamble -->
|
|
6
|
+
**Docs:** [Website](https://dotmonk.github.io/wildling/) · [Sandbox](https://dotmonk.github.io/wildling/sandbox.html) · [Syntax](https://dotmonk.github.io/wildling/syntax.html) · [Source](https://github.com/dotmonk/wildling/tree/main/ruby)
|
|
7
|
+
|
|
8
|
+
**Registry:** [RubyGems](https://rubygems.org/gems/wildling)
|
|
9
|
+
|
|
10
|
+
## Example
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
http://${'dev,stage,prod'}\-${'api,web'}#{0-2}.example.${'com,net,org'}/@.html
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
(The `\-` is a literal hyphen; bare `-` would mean “one letter or digit”. `@` is one lowercase letter.)
|
|
17
|
+
|
|
18
|
+
That builds **URL-shaped** candidates: scheme `http://`, then environment × service × optional digits × TLD, then a one-letter path page. Three environments, two services, zero–two digits (`''`, `0`–`9`, `00`–`99`), three TLDs, and `a`–`z` → **51948** strings — the kind of list you generate for fuzzing links or probing staging hosts, not type out.
|
|
19
|
+
|
|
20
|
+
A few of them:
|
|
21
|
+
|
|
22
|
+
- `http://dev-api.example.com/a.html` / `http://stage-web.example.com/z.html`
|
|
23
|
+
- `http://dev-api0.example.net/a.html` / `http://prod-web9.example.org/m.html`
|
|
24
|
+
- `http://dev-api00.example.com/a.html` / `http://prod-web99.example.org/z.html`
|
|
25
|
+
|
|
26
|
+
Named dictionaries (`%{'hosts'}`) work the same way when the word lists live in files.
|
|
27
|
+
|
|
28
|
+
Try it in the [sandbox](https://dotmonk.github.io/wildling/sandbox.html?pattern=http%3A%2F%2F%24%7B%27dev%2Cstage%2Cprod%27%7D%5C-%24%7B%27api%2Cweb%27%7D%23%7B0-2%7D.example.%24%7B%27com%2Cnet%2Corg%27%7D%2F%40.html), or see [pattern syntax](https://dotmonk.github.io/wildling/syntax.html) for length ranges, dictionaries, and escapes.
|
|
29
|
+
<!-- /wildling:preamble -->
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
From this repository:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd ruby
|
|
37
|
+
./build.sh
|
|
38
|
+
./bin/wildling "Year 19##"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Git (Bundler):**
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
gem "wildling", git: "https://github.com/dotmonk/wildling.git", tag: "v2.0.0", glob: "ruby/wildling.gemspec"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Registry:** `gem install wildling`
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require_relative "lib/wildling"
|
|
51
|
+
|
|
52
|
+
wildling = Wildling.create(["Year 19##"])
|
|
53
|
+
value = wildling.next
|
|
54
|
+
while value != false
|
|
55
|
+
puts value
|
|
56
|
+
value = wildling.next
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
./bin/wildling "Year 19##"
|
|
64
|
+
./bin/wildling --dictionary planets:../dictionaries/planets.txt "%{'planets'}"
|
|
65
|
+
./bin/wildling --template ./config.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Help text and `--check` output follow [`docs/cli.md`](../docs/cli.md) / [`docs/help.txt`](../docs/help.txt).
|
|
69
|
+
|
|
70
|
+
## Build
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
./build.sh # Docker (ruby:3.3-alpine): copy help.txt + syntax-check
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Project tests live in `../tests/` and are run with `../test.sh`.
|
data/bin/wildling
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
3
|
+
ROOT="$(cd "$DIR/.." && pwd)"
|
|
4
|
+
ENTRY="$DIR/wildling.rb"
|
|
5
|
+
|
|
6
|
+
ensure_help() {
|
|
7
|
+
if [ ! -f "$ROOT/lib/wildling/help.txt" ] && [ -f "$ROOT/../docs/help.txt" ]; then
|
|
8
|
+
cp "$ROOT/../docs/help.txt" "$ROOT/lib/wildling/help.txt"
|
|
9
|
+
fi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if command -v ruby >/dev/null 2>&1; then
|
|
13
|
+
ensure_help
|
|
14
|
+
exec ruby "$ENTRY" "$@"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
ensure_help
|
|
18
|
+
if [ ! -f "$ROOT/lib/wildling/help.txt" ]; then
|
|
19
|
+
echo "help.txt missing. Run ruby/build.sh first." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
exec docker run --rm \
|
|
24
|
+
-v "$ROOT:/app:ro" \
|
|
25
|
+
-v "$(pwd):/work" \
|
|
26
|
+
-w /work \
|
|
27
|
+
--network=host \
|
|
28
|
+
ruby:3.3-alpine \
|
|
29
|
+
ruby /app/bin/wildling.rb "$@"
|
data/bin/wildling.rb
ADDED
data/help.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
wildling - pattern based string generator
|
|
2
|
+
|
|
3
|
+
USAGE
|
|
4
|
+
wildling [options] [pattern ...]
|
|
5
|
+
|
|
6
|
+
OPTIONS
|
|
7
|
+
--select # Print only combination number # (repeatable)
|
|
8
|
+
--range #-# Print combinations from # to # inclusive (repeatable)
|
|
9
|
+
--check Print generation info instead of results
|
|
10
|
+
--dictionary <name>:<path> Load a dictionary file as <name> (repeatable)
|
|
11
|
+
--template <path> Load options from a JSON template file
|
|
12
|
+
--help, -h Show this help message
|
|
13
|
+
--version, -v Show version
|
|
14
|
+
|
|
15
|
+
PATTERNS
|
|
16
|
+
# #{N} #{N-M} digits 0-9; optional fixed or ranged length
|
|
17
|
+
@ @{N} @{N-M} lowercase a-z
|
|
18
|
+
* *{N} *{N-M} lowercase a-z and digits 0-9
|
|
19
|
+
& &{N} &{N-M} lowercase and uppercase letters
|
|
20
|
+
! !{N} !{N-M} uppercase A-Z
|
|
21
|
+
? ?{N} ?{N-M} uppercase A-Z and digits 0-9
|
|
22
|
+
- -{N} -{N-M} letters and digits
|
|
23
|
+
${'a,b',N-M} combinations from a comma-separated word list
|
|
24
|
+
%{'name'} %{'name',N-M} words from a named dictionary
|
|
25
|
+
\# \@ \$ ... escape a wildcard character
|
|
26
|
+
|
|
27
|
+
TEMPLATE JSON
|
|
28
|
+
{
|
|
29
|
+
"patterns": ["Year 19##", "%{'colors'}"],
|
|
30
|
+
"dictionaries": {
|
|
31
|
+
"colors": "path/to/colors.txt",
|
|
32
|
+
"inline": ["red", "blue"]
|
|
33
|
+
},
|
|
34
|
+
"select": [0, 2],
|
|
35
|
+
"range": ["5-7"],
|
|
36
|
+
"check": false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
EXAMPLES
|
|
40
|
+
wildling --dictionary colors:./colors.txt "%{'colors'}#"
|
|
41
|
+
wildling --template ./config.json
|
|
42
|
+
wildling --select 0 --range 8-9 "##"
|
|
43
|
+
wildling "${'blue,red',1-2}"
|
data/lib/wildling/cli.rb
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "wildling"
|
|
5
|
+
|
|
6
|
+
module Wildling
|
|
7
|
+
module Cli
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
CliArgs = Struct.new(
|
|
11
|
+
:selects, :ranges, :check, :dictionaries, :patterns, :help, :version,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
def parse_range(value)
|
|
16
|
+
parts = value.split("-", 2)
|
|
17
|
+
return nil if parts.length != 2 || !parts[0].match?(/\A\d+\z/) || !parts[1].match?(/\A\d+\z/)
|
|
18
|
+
|
|
19
|
+
start = parts[0].to_i
|
|
20
|
+
finish = parts[1].to_i
|
|
21
|
+
start <= finish ? [start, finish] : nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def load_dictionary_file(path)
|
|
25
|
+
File.read(path, encoding: "UTF-8").split(/\r?\n/).map(&:strip).reject(&:empty?)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def apply_dictionary(result, name, value)
|
|
29
|
+
if value.is_a?(Array)
|
|
30
|
+
result.dictionaries[name] = value.map(&:to_s)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
return unless value.is_a?(String) && File.exist?(value)
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
result.dictionaries[name] = load_dictionary_file(value)
|
|
37
|
+
rescue SystemCallError
|
|
38
|
+
# ignore unreadable dictionary files
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def apply_template(result, path)
|
|
43
|
+
unless File.exist?(path)
|
|
44
|
+
warn "Template file not found: #{path}"
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
template = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
50
|
+
rescue SystemCallError, JSON::ParserError
|
|
51
|
+
warn "Invalid JSON template: #{path}"
|
|
52
|
+
exit 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless template.is_a?(Hash)
|
|
56
|
+
warn "Invalid JSON template: #{path}"
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result.check = true if template["check"] == true
|
|
61
|
+
|
|
62
|
+
select = template["select"]
|
|
63
|
+
if select.is_a?(Array)
|
|
64
|
+
select.each do |val|
|
|
65
|
+
number = Integer(val)
|
|
66
|
+
result.selects << number if number >= 0
|
|
67
|
+
rescue ArgumentError, TypeError
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
ranges = template["range"]
|
|
73
|
+
if ranges.is_a?(Array)
|
|
74
|
+
ranges.each do |range_str|
|
|
75
|
+
parsed = parse_range(range_str.to_s)
|
|
76
|
+
result.ranges << parsed if parsed
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
dictionaries = template["dictionaries"]
|
|
81
|
+
if dictionaries.is_a?(Hash)
|
|
82
|
+
dictionaries.each do |name, value|
|
|
83
|
+
apply_dictionary(result, name.to_s, value) if value.is_a?(String) || value.is_a?(Array)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
patterns = template["patterns"]
|
|
88
|
+
if patterns.is_a?(Array)
|
|
89
|
+
patterns.each { |pattern| result.patterns << pattern.to_s }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_args(args)
|
|
94
|
+
result = CliArgs.new(
|
|
95
|
+
selects: [],
|
|
96
|
+
ranges: [],
|
|
97
|
+
check: false,
|
|
98
|
+
dictionaries: {},
|
|
99
|
+
patterns: [],
|
|
100
|
+
help: false,
|
|
101
|
+
version: false
|
|
102
|
+
)
|
|
103
|
+
i = 0
|
|
104
|
+
while i < args.length
|
|
105
|
+
arg = args[i]
|
|
106
|
+
|
|
107
|
+
case arg
|
|
108
|
+
when "--help", "-h"
|
|
109
|
+
result.help = true
|
|
110
|
+
i += 1
|
|
111
|
+
when "--version", "-v"
|
|
112
|
+
result.version = true
|
|
113
|
+
i += 1
|
|
114
|
+
when "--check"
|
|
115
|
+
result.check = true
|
|
116
|
+
i += 1
|
|
117
|
+
when "--select"
|
|
118
|
+
i += 1
|
|
119
|
+
break if i >= args.length
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
val = Integer(args[i])
|
|
123
|
+
result.selects << val if val >= 0
|
|
124
|
+
rescue ArgumentError, TypeError
|
|
125
|
+
# skip invalid select
|
|
126
|
+
end
|
|
127
|
+
i += 1
|
|
128
|
+
when "--range"
|
|
129
|
+
i += 1
|
|
130
|
+
break if i >= args.length
|
|
131
|
+
|
|
132
|
+
parsed = parse_range(args[i])
|
|
133
|
+
result.ranges << parsed if parsed
|
|
134
|
+
i += 1
|
|
135
|
+
when "--dictionary"
|
|
136
|
+
i += 1
|
|
137
|
+
break if i >= args.length
|
|
138
|
+
|
|
139
|
+
name, path = args[i].split(":", 2)
|
|
140
|
+
apply_dictionary(result, name, path) if name && path && !name.empty? && !path.empty?
|
|
141
|
+
i += 1
|
|
142
|
+
when "--template"
|
|
143
|
+
i += 1
|
|
144
|
+
if i >= args.length
|
|
145
|
+
warn "Missing path for --template"
|
|
146
|
+
exit 1
|
|
147
|
+
end
|
|
148
|
+
apply_template(result, args[i])
|
|
149
|
+
i += 1
|
|
150
|
+
else
|
|
151
|
+
result.patterns << arg
|
|
152
|
+
i += 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
result
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def load_help_text
|
|
159
|
+
here = File.expand_path(__dir__)
|
|
160
|
+
candidates = [
|
|
161
|
+
File.join(here, "help.txt"),
|
|
162
|
+
File.join(here, "..", "..", "docs", "help.txt")
|
|
163
|
+
]
|
|
164
|
+
candidates.each do |path|
|
|
165
|
+
return File.read(path, encoding: "UTF-8") if File.exist?(path)
|
|
166
|
+
end
|
|
167
|
+
"wildling - pattern based string generator\n\nHelp text unavailable.\n"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def format_list(values)
|
|
171
|
+
values.nil? || values.empty? ? "" : " #{values.map(&:to_s).join(' ')}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_check_output(args, total, generators)
|
|
175
|
+
lines = [
|
|
176
|
+
"patterns:#{format_list(args.patterns)}",
|
|
177
|
+
"dictionaries:#{format_list(args.dictionaries.keys)}",
|
|
178
|
+
"select:#{format_list(args.selects)}",
|
|
179
|
+
"range:#{format_list(args.ranges.map { |start, finish| "#{start}-#{finish}" })}",
|
|
180
|
+
"total: #{total}"
|
|
181
|
+
]
|
|
182
|
+
generators.each do |gen|
|
|
183
|
+
lines << "generator: #{gen.source} #{gen.count}"
|
|
184
|
+
end
|
|
185
|
+
lines.join("\n")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def main(argv = nil)
|
|
189
|
+
args = parse_args(argv || ARGV)
|
|
190
|
+
|
|
191
|
+
if args.help
|
|
192
|
+
puts load_help_text.rstrip
|
|
193
|
+
exit 0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if args.version
|
|
197
|
+
puts "wildling #{VERSION}"
|
|
198
|
+
exit 0
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if args.patterns.empty?
|
|
202
|
+
warn "No pattern provided. Use --help for usage information."
|
|
203
|
+
exit 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
wildcard = ::Wildling.create(args.patterns, args.dictionaries)
|
|
207
|
+
|
|
208
|
+
if args.check
|
|
209
|
+
puts format_check_output(args, wildcard.count, wildcard.generators)
|
|
210
|
+
exit 0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if !args.selects.empty? || !args.ranges.empty?
|
|
214
|
+
oor = false
|
|
215
|
+
args.selects.each do |index|
|
|
216
|
+
value = wildcard.get(index)
|
|
217
|
+
if value == false
|
|
218
|
+
$stderr.puts "out of range: #{index}"
|
|
219
|
+
oor = true
|
|
220
|
+
else
|
|
221
|
+
puts value
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
args.ranges.each do |start, finish|
|
|
225
|
+
(start..finish).each do |index|
|
|
226
|
+
value = wildcard.get(index)
|
|
227
|
+
if value == false
|
|
228
|
+
$stderr.puts "out of range: #{index}"
|
|
229
|
+
oor = true
|
|
230
|
+
else
|
|
231
|
+
puts value
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
exit(oor ? 1 : 0)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
value = wildcard.next
|
|
239
|
+
while value != false
|
|
240
|
+
puts value
|
|
241
|
+
value = wildcard.next
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parse_pattern"
|
|
4
|
+
|
|
5
|
+
module Wildling
|
|
6
|
+
class Generator
|
|
7
|
+
attr_reader :source
|
|
8
|
+
|
|
9
|
+
def initialize(input_pattern, dictionaries)
|
|
10
|
+
@source = input_pattern
|
|
11
|
+
@tokens = ::Wildling.parse_pattern(input_pattern, dictionaries)
|
|
12
|
+
@count = 1
|
|
13
|
+
@tokens.each { |token| @count *= token.count }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def count
|
|
17
|
+
@count
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tokens
|
|
21
|
+
@tokens
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(index)
|
|
25
|
+
return "" if index > @count - 1 || index < 0
|
|
26
|
+
|
|
27
|
+
string_array = []
|
|
28
|
+
index_with_offset = index
|
|
29
|
+
@tokens.each do |token|
|
|
30
|
+
string_array << token.get(index_with_offset % token.count)
|
|
31
|
+
index_with_offset /= token.count
|
|
32
|
+
end
|
|
33
|
+
string_array.join
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "token"
|
|
4
|
+
|
|
5
|
+
module Wildling
|
|
6
|
+
TOKEN_PARSING_REGEX = /(\\[%@$*#&?!-]|[%@$*#&?!-]\{.*?\}|[%@$*#&?!-])/
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def parse_length_with_variants(part, variants)
|
|
11
|
+
match = part.match(/\{((\d+)-(\d+)|(\d+))\}/)
|
|
12
|
+
|
|
13
|
+
start_length = 1
|
|
14
|
+
end_length = 1
|
|
15
|
+
|
|
16
|
+
if match
|
|
17
|
+
if match[2]
|
|
18
|
+
start_length = match[2].to_i
|
|
19
|
+
end_length = match[3].to_i
|
|
20
|
+
elsif match[1]
|
|
21
|
+
start_length = match[1].to_i
|
|
22
|
+
end_length = start_length
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"variants" => variants,
|
|
28
|
+
"startLength" => start_length,
|
|
29
|
+
"endLength" => end_length,
|
|
30
|
+
"src" => part
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_length_with_string(part)
|
|
35
|
+
match = part.match(/\{'(.*)'(?:,(\d+)-(\d+))?(?:,(\d+))?\}/)
|
|
36
|
+
return false unless match
|
|
37
|
+
|
|
38
|
+
if match[2] && match[3]
|
|
39
|
+
return {
|
|
40
|
+
"string" => match[1] || "",
|
|
41
|
+
"startLength" => match[2].to_i,
|
|
42
|
+
"endLength" => match[3].to_i,
|
|
43
|
+
"src" => part
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if match[4]
|
|
48
|
+
length = match[4].to_i
|
|
49
|
+
return {
|
|
50
|
+
"string" => match[1] || "",
|
|
51
|
+
"startLength" => length,
|
|
52
|
+
"endLength" => length,
|
|
53
|
+
"src" => part
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
"string" => match[1] || "",
|
|
59
|
+
"startLength" => 1,
|
|
60
|
+
"endLength" => 1,
|
|
61
|
+
"src" => part
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def simple_tokenizer(variants_string)
|
|
66
|
+
variants = variants_string.chars
|
|
67
|
+
lambda do |part|
|
|
68
|
+
Token.new(parse_length_with_variants(part, variants))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def dictionary_tokenizer(part, dictionaries)
|
|
73
|
+
options = parse_length_with_string(part)
|
|
74
|
+
if options == false || (options["string"] && !options["string"].empty? && !dictionaries.key?(options["string"]))
|
|
75
|
+
options = {
|
|
76
|
+
"variants" => [part],
|
|
77
|
+
"startLength" => 1,
|
|
78
|
+
"endLength" => 1,
|
|
79
|
+
"src" => part
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
options["variants"] = dictionaries[options["string"] || ""] || []
|
|
83
|
+
end
|
|
84
|
+
Token.new(options)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def words_tokenizer(part)
|
|
88
|
+
options = parse_length_with_string(part)
|
|
89
|
+
|
|
90
|
+
if options == false
|
|
91
|
+
options = {
|
|
92
|
+
"variants" => [part],
|
|
93
|
+
"startLength" => 1,
|
|
94
|
+
"endLength" => 1,
|
|
95
|
+
"src" => part
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
variants = []
|
|
99
|
+
work_string = options["string"] || ""
|
|
100
|
+
index = 0
|
|
101
|
+
while index < work_string.length
|
|
102
|
+
if work_string[index, 2] == "\\,"
|
|
103
|
+
index += 2
|
|
104
|
+
elsif work_string[index] == ","
|
|
105
|
+
variants << work_string[0...index]
|
|
106
|
+
work_string = work_string[(index + 1)..] || ""
|
|
107
|
+
index = 0
|
|
108
|
+
else
|
|
109
|
+
index += 1
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
variants << work_string
|
|
113
|
+
options["variants"] = variants.map { |variant| variant.gsub("\\,", ",") }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Token.new(options)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def part_to_token(part, dictionaries)
|
|
120
|
+
tokenizers = {
|
|
121
|
+
"#" => simple_tokenizer("0123456789"),
|
|
122
|
+
"@" => simple_tokenizer("abcdefghijklmnopqrstuvwxyz"),
|
|
123
|
+
"*" => simple_tokenizer("abcdefghijklmnopqrstuvwxyz0123456789"),
|
|
124
|
+
"-" => simple_tokenizer(
|
|
125
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
126
|
+
),
|
|
127
|
+
"!" => simple_tokenizer("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
|
128
|
+
"?" => simple_tokenizer("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"),
|
|
129
|
+
"&" => simple_tokenizer("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
|
130
|
+
"%" => ->(p) { dictionary_tokenizer(p, dictionaries) },
|
|
131
|
+
"$" => method(:words_tokenizer)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tokenizer = part.empty? ? nil : tokenizers[part[0]]
|
|
135
|
+
is_escaped = part.length > 1 && part[0] == "\\" && tokenizers.key?(part[1])
|
|
136
|
+
|
|
137
|
+
if tokenizer
|
|
138
|
+
tokenizer.call(part)
|
|
139
|
+
elsif is_escaped
|
|
140
|
+
Token.new(
|
|
141
|
+
"variants" => [part.sub(/^\\/, "")],
|
|
142
|
+
"src" => part
|
|
143
|
+
)
|
|
144
|
+
else
|
|
145
|
+
Token.new(
|
|
146
|
+
"variants" => [part],
|
|
147
|
+
"src" => part
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_pattern(input_pattern, dictionaries)
|
|
153
|
+
dictionaries ||= {}
|
|
154
|
+
parts = input_pattern.split(TOKEN_PARSING_REGEX).reject(&:empty?)
|
|
155
|
+
parts.map { |part| part_to_token(part, dictionaries) }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wildling
|
|
4
|
+
class Token
|
|
5
|
+
def initialize(options)
|
|
6
|
+
options = symbolize_keys(options)
|
|
7
|
+
@src = options.fetch(:src, "")
|
|
8
|
+
@start_length = default_integer(options[:startLength], 1)
|
|
9
|
+
@end_length = default_integer(options[:endLength], 1)
|
|
10
|
+
@variants = options[:variants] || []
|
|
11
|
+
@count = 0
|
|
12
|
+
(@start_length..@end_length).each do |length|
|
|
13
|
+
@count += @variants.length**length
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def count
|
|
18
|
+
@count
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def src
|
|
22
|
+
@src
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def get(index)
|
|
26
|
+
return "" if index > @count - 1 || index < 0
|
|
27
|
+
return "" if index.zero? && @start_length.zero?
|
|
28
|
+
|
|
29
|
+
index_with_offset = index
|
|
30
|
+
string_length = @start_length
|
|
31
|
+
(@start_length..@end_length).each do |length|
|
|
32
|
+
string_length = length
|
|
33
|
+
offset_count = @variants.length**length
|
|
34
|
+
break if index_with_offset < offset_count
|
|
35
|
+
|
|
36
|
+
index_with_offset -= offset_count
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
string_array = []
|
|
40
|
+
string_length.times do
|
|
41
|
+
variant_index = index_with_offset % @variants.length
|
|
42
|
+
index_with_offset /= @variants.length
|
|
43
|
+
string_array << @variants[variant_index]
|
|
44
|
+
end
|
|
45
|
+
string_array.join
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def default_integer(option, fallback)
|
|
51
|
+
option.is_a?(Integer) && option >= 0 ? option : fallback
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def symbolize_keys(hash)
|
|
55
|
+
hash.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "generator"
|
|
4
|
+
|
|
5
|
+
module Wildling
|
|
6
|
+
VERSION = "2.0.2"
|
|
7
|
+
|
|
8
|
+
# Main enumerator. Prefer Wildling.create(...) from callers.
|
|
9
|
+
class Client
|
|
10
|
+
def initialize(patterns, dictionaries = nil)
|
|
11
|
+
@dictionaries = dictionaries || {}
|
|
12
|
+
@generators = patterns.map { |pattern| Generator.new(pattern, @dictionaries) }
|
|
13
|
+
@pattern_count = @generators.sum(&:count)
|
|
14
|
+
@internal_index = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def index
|
|
18
|
+
@internal_index
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def count
|
|
22
|
+
@pattern_count
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset
|
|
26
|
+
@internal_index = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def next
|
|
30
|
+
return false if @internal_index == @pattern_count
|
|
31
|
+
|
|
32
|
+
@internal_index += 1
|
|
33
|
+
get(@internal_index - 1)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generators
|
|
37
|
+
@generators
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get(index)
|
|
41
|
+
return false if index > @pattern_count - 1 || index < 0
|
|
42
|
+
|
|
43
|
+
segment_index = 0
|
|
44
|
+
@generators.each do |generator|
|
|
45
|
+
pattern_index = index - segment_index
|
|
46
|
+
return generator.get(pattern_index) if pattern_index < generator.count
|
|
47
|
+
|
|
48
|
+
segment_index += generator.count
|
|
49
|
+
end
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.create(patterns, dictionaries = nil)
|
|
55
|
+
Client.new(patterns, dictionaries)
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/wildling.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wildling
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- dotmonk
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Enumerate pattern combinations for wordlists, domains, and test data.
|
|
14
|
+
email:
|
|
15
|
+
- dotmonk@users.noreply.github.com
|
|
16
|
+
executables:
|
|
17
|
+
- wildling
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- bin/wildling
|
|
23
|
+
- bin/wildling.rb
|
|
24
|
+
- help.txt
|
|
25
|
+
- lib/wildling.rb
|
|
26
|
+
- lib/wildling/cli.rb
|
|
27
|
+
- lib/wildling/generator.rb
|
|
28
|
+
- lib/wildling/parse_pattern.rb
|
|
29
|
+
- lib/wildling/token.rb
|
|
30
|
+
- lib/wildling/wildling.rb
|
|
31
|
+
homepage: https://github.com/dotmonk/wildling
|
|
32
|
+
licenses:
|
|
33
|
+
- MIT
|
|
34
|
+
metadata:
|
|
35
|
+
homepage_uri: https://github.com/dotmonk/wildling
|
|
36
|
+
source_code_uri: https://github.com/dotmonk/wildling/tree/main/ruby
|
|
37
|
+
changelog_uri: https://github.com/dotmonk/wildling/releases
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.0.0
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.5.22
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: Pattern based string generator library and CLI
|
|
57
|
+
test_files: []
|