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 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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ root = File.expand_path("..", __dir__)
5
+ $LOAD_PATH.unshift(File.join(root, "lib"))
6
+
7
+ require "wildling"
8
+
9
+ Wildling::Cli.main
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}"
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "wildling/token"
4
+ require_relative "wildling/parse_pattern"
5
+ require_relative "wildling/generator"
6
+ require_relative "wildling/wildling"
7
+ require_relative "wildling/cli"
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: []