obfs 0.1.2 → 0.2.0
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/lib/obfs/store.rb +999 -114
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 648385c40ccd4ea5cb5f608fc7164e1c86757f5ec6c2053711ebf71eab7fa752
|
|
4
|
+
data.tar.gz: f89b3c016e0949fab62abf7187970db6e16524e927dfa5a98747a6e3d6e7bc83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6617be8b4f8d2c57f9ad353eb5954cf66604c1bfcf03dac73465d0e2854257e52efe979225a55907e75c6be1e59834fbb55f1ba80983bb638c4639f05065569
|
|
7
|
+
data.tar.gz: 02f0c229b2cff282033822c0d3502b006714ec361411f284123b971bf04ee1f8f94bbb77409bd39a7b66205adf95d2fe8f464fe8d3a24779720148457256fab2
|
data/lib/obfs/store.rb
CHANGED
|
@@ -1,132 +1,1017 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
1
4
|
module OBFS
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
class Store
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
def initialize(attributes = {})
|
|
9
|
+
@path = (attributes.keys.include? :path) ? attributes[:path] : File.join(Dir.home, '.obfs')
|
|
10
|
+
end
|
|
8
11
|
|
|
9
|
-
# regular methods
|
|
10
|
-
|
|
11
|
-
def method_missing(m, *args, &block)
|
|
12
|
-
|
|
13
|
-
# normalize
|
|
14
|
-
method_name = m.to_s
|
|
15
|
-
dataA = args[0]
|
|
16
|
-
dataB = args[1]
|
|
17
|
-
|
|
18
|
-
# prevent traversing out of dir
|
|
19
|
-
raise "traversal through . and .. not allowed" if ['.', '..'].include? method_name
|
|
20
|
-
|
|
21
|
-
# setter call
|
|
22
|
-
if method_name.end_with?('=')
|
|
23
|
-
|
|
24
|
-
# clean up name
|
|
25
|
-
method_name = method_name.gsub('=','')
|
|
26
|
-
|
|
27
|
-
# reassign if square bracket notation
|
|
28
|
-
if method_name == "[]"
|
|
29
|
-
method_name = dataA
|
|
30
|
-
data = dataB
|
|
31
|
-
else # make sure we load the proper method_name and data
|
|
32
|
-
method_name = m.to_s.gsub('=','')
|
|
33
|
-
data = args[0]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# prevent traversing out of dir
|
|
37
|
-
raise "traversal through . and .. not allowed" if ['.', '..'].include? method_name
|
|
38
|
-
|
|
39
|
-
# write data
|
|
40
|
-
if data == nil
|
|
41
|
-
FileUtils.rm_rf (File.join @path, method_name)
|
|
42
|
-
else
|
|
43
|
-
FileUtils.rm_rf (File.join @path, method_name) if File.exist? (File.join @path, method_name)
|
|
44
|
-
FileUtils.mkpath @path if !File.directory? @path
|
|
45
|
-
write(@path, method_name, data)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# bracket notation
|
|
49
|
-
elsif method_name == "[]"
|
|
50
|
-
|
|
51
|
-
method_name = dataA.to_s.gsub(/\["/,'').gsub(/"\]/,'')
|
|
52
|
-
|
|
53
|
-
# prevent traversing out of dir
|
|
54
|
-
raise "traversal through . and .. not allowed" if ['.', '..'].include? method_name
|
|
55
|
-
|
|
56
|
-
if (!File.directory? File.join(@path, method_name)) && (File.exist? File.join(@path, method_name))
|
|
57
|
-
read(@path, method_name)
|
|
58
|
-
else
|
|
59
|
-
OBFS::Store.new({ path: File.join(@path, method_name.to_s) })
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# recurse or read
|
|
63
|
-
else
|
|
64
|
-
|
|
65
|
-
# prevent traversing out of dir
|
|
66
|
-
raise "traversal through . and .. not allowed" if ['.', '..'].include? method_name
|
|
67
|
-
|
|
68
|
-
if (!File.directory? File.join(@path, method_name)) && (File.exist? File.join(@path, method_name))
|
|
69
|
-
read(@path, method_name)
|
|
70
|
-
else
|
|
71
|
-
OBFS::Store.new({ path: File.join(@path, method_name.to_s) })
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
end
|
|
77
12
|
|
|
78
|
-
|
|
13
|
+
# ============================================================
|
|
14
|
+
# REGULAR METHODS
|
|
15
|
+
# ============================================================
|
|
79
16
|
|
|
80
|
-
|
|
81
|
-
def _path
|
|
82
|
-
@path
|
|
83
|
-
end
|
|
17
|
+
def method_missing(m, *args, &block)
|
|
84
18
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
19
|
+
method_name = m.to_s
|
|
20
|
+
dataA = args[0]
|
|
21
|
+
dataB = args[1]
|
|
89
22
|
|
|
90
|
-
# searches directory contents (1 level) and returns array sorted by relevance
|
|
91
|
-
def _find(term = '', records = 1000, tolerance = 50)
|
|
92
|
-
output = []
|
|
93
|
-
search_space = Dir.entries(@path).reject { |k| k == '.' || k == '..' } rescue []
|
|
94
|
-
search_space.each do |search_space_term|
|
|
95
|
-
if OBFS::Levenshtein.distance(search_space_term, term) <= tolerance && OBFS::WhiteSimilarity.similarity(search_space_term, term) > 0.0
|
|
96
|
-
output << search_space_term
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
output.first(records)
|
|
100
|
-
end
|
|
101
23
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
else
|
|
109
|
-
false
|
|
110
|
-
end
|
|
111
|
-
else
|
|
112
|
-
false
|
|
113
|
-
end
|
|
114
|
-
end
|
|
24
|
+
# ----------------------------------------------------------
|
|
25
|
+
# setter call
|
|
26
|
+
#
|
|
27
|
+
# obfs.foo = 'bar'
|
|
28
|
+
# obfs['foo'] = 'bar'
|
|
29
|
+
# ----------------------------------------------------------
|
|
115
30
|
|
|
116
|
-
|
|
31
|
+
if method_name.end_with?('=')
|
|
117
32
|
|
|
118
|
-
|
|
33
|
+
method_name = method_name.gsub('=', '')
|
|
119
34
|
|
|
120
|
-
def write(path, filename, data)
|
|
121
|
-
curr_path = File.join path, filename
|
|
122
|
-
File.write(curr_path, JSON.dump(data))
|
|
123
|
-
end
|
|
124
35
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
36
|
+
# bracket setter
|
|
37
|
+
if method_name == "[]"
|
|
38
|
+
|
|
39
|
+
method_name = validate_child_name!(dataA)
|
|
40
|
+
data = dataB
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# dot setter
|
|
44
|
+
else
|
|
45
|
+
|
|
46
|
+
method_name = validate_child_name!(
|
|
47
|
+
m.to_s.gsub('=', '')
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
data = args[0]
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# delete
|
|
56
|
+
if data == nil
|
|
57
|
+
|
|
58
|
+
FileUtils.rm_rf(
|
|
59
|
+
File.join(
|
|
60
|
+
@path,
|
|
61
|
+
method_name
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# write
|
|
67
|
+
else
|
|
68
|
+
|
|
69
|
+
child_path =
|
|
70
|
+
File.join(
|
|
71
|
+
@path,
|
|
72
|
+
method_name
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
FileUtils.rm_rf(child_path) if File.exist?(child_path)
|
|
77
|
+
|
|
78
|
+
FileUtils.mkpath @path if !File.directory? @path
|
|
79
|
+
|
|
80
|
+
write(
|
|
81
|
+
@path,
|
|
82
|
+
method_name,
|
|
83
|
+
data
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ----------------------------------------------------------
|
|
90
|
+
# bracket getter
|
|
91
|
+
#
|
|
92
|
+
# obfs['foo']
|
|
93
|
+
# ----------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
elsif method_name == "[]"
|
|
96
|
+
|
|
97
|
+
method_name =
|
|
98
|
+
dataA
|
|
99
|
+
.to_s
|
|
100
|
+
.gsub(/\["/, '')
|
|
101
|
+
.gsub(/"\]/, '')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
method_name =
|
|
105
|
+
validate_child_name!(
|
|
106
|
+
method_name
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
child_path =
|
|
111
|
+
File.join(
|
|
112
|
+
@path,
|
|
113
|
+
method_name
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if !File.directory?(child_path) &&
|
|
118
|
+
File.exist?(child_path)
|
|
119
|
+
|
|
120
|
+
read(
|
|
121
|
+
@path,
|
|
122
|
+
method_name
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
else
|
|
126
|
+
|
|
127
|
+
OBFS::Store.new({
|
|
128
|
+
path: child_path
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ----------------------------------------------------------
|
|
135
|
+
# dot getter / recursive access
|
|
136
|
+
#
|
|
137
|
+
# obfs.foo.bar
|
|
138
|
+
# ----------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
else
|
|
141
|
+
|
|
142
|
+
method_name =
|
|
143
|
+
validate_child_name!(
|
|
144
|
+
method_name
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
child_path =
|
|
149
|
+
File.join(
|
|
150
|
+
@path,
|
|
151
|
+
method_name
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if !File.directory?(child_path) &&
|
|
156
|
+
File.exist?(child_path)
|
|
157
|
+
|
|
158
|
+
read(
|
|
159
|
+
@path,
|
|
160
|
+
method_name
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
else
|
|
164
|
+
|
|
165
|
+
OBFS::Store.new({
|
|
166
|
+
path: child_path
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ============================================================
|
|
177
|
+
# SPECIAL METHODS
|
|
178
|
+
# ============================================================
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------
|
|
182
|
+
# current path
|
|
183
|
+
#
|
|
184
|
+
# obfs._path
|
|
185
|
+
# ------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _path
|
|
188
|
+
@path
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------
|
|
193
|
+
# directory contents
|
|
194
|
+
#
|
|
195
|
+
# Existing behavior preserved:
|
|
196
|
+
#
|
|
197
|
+
# - excludes "." and ".."
|
|
198
|
+
# - includes dot-prefixed entries
|
|
199
|
+
# - preserves filesystem ordering
|
|
200
|
+
# - returns nil on failure
|
|
201
|
+
#
|
|
202
|
+
# obfs._index
|
|
203
|
+
# obfs._keys
|
|
204
|
+
# ------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
def _index
|
|
207
|
+
|
|
208
|
+
Dir.entries(@path).reject {
|
|
209
|
+
|k| k == '.' || k == '..'
|
|
210
|
+
} rescue nil
|
|
211
|
+
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
alias_method :_keys, :_index
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ------------------------------------------------------------
|
|
218
|
+
# one-level fuzzy search
|
|
219
|
+
#
|
|
220
|
+
# Existing signature and return type preserved:
|
|
221
|
+
#
|
|
222
|
+
# obfs._find(term, records, tolerance)
|
|
223
|
+
#
|
|
224
|
+
# Returns an Array<String>, but now actually sorted by
|
|
225
|
+
# relevance as originally intended.
|
|
226
|
+
# ------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def _find(term = '', records = 1000, tolerance = 50)
|
|
229
|
+
|
|
230
|
+
term = term.to_s
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
search_space =
|
|
234
|
+
Dir.entries(@path).reject {
|
|
235
|
+
|k| k == '.' || k == '..'
|
|
236
|
+
} rescue []
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
results = []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
search_space.each do |search_space_term|
|
|
243
|
+
|
|
244
|
+
distance =
|
|
245
|
+
OBFS::Levenshtein.distance(
|
|
246
|
+
search_space_term,
|
|
247
|
+
term
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
similarity =
|
|
252
|
+
OBFS::WhiteSimilarity.similarity(
|
|
253
|
+
search_space_term,
|
|
254
|
+
term
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if distance <= tolerance &&
|
|
259
|
+
similarity > 0.0
|
|
260
|
+
|
|
261
|
+
results << {
|
|
262
|
+
name: search_space_term,
|
|
263
|
+
score: fuzzy_score(
|
|
264
|
+
term,
|
|
265
|
+
search_space_term
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
results
|
|
275
|
+
.sort_by {
|
|
276
|
+
|result|
|
|
277
|
+
|
|
278
|
+
[
|
|
279
|
+
-result[:score],
|
|
280
|
+
result[:name].downcase
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
.first(records)
|
|
284
|
+
.map {
|
|
285
|
+
|result|
|
|
286
|
+
|
|
287
|
+
result[:name]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------
|
|
294
|
+
# existing direct-child existence check
|
|
295
|
+
#
|
|
296
|
+
# Existing behavior preserved.
|
|
297
|
+
#
|
|
298
|
+
# obfs._exist('foo')
|
|
299
|
+
# ------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def _exist(term = '')
|
|
302
|
+
|
|
303
|
+
exist_space =
|
|
304
|
+
Dir.entries(@path).reject {
|
|
305
|
+
|k|
|
|
306
|
+
|
|
307
|
+
k != term.to_s ||
|
|
308
|
+
k == '.' ||
|
|
309
|
+
k == '..'
|
|
310
|
+
} rescue nil
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if !exist_space.nil?
|
|
314
|
+
|
|
315
|
+
if exist_space.length > 0
|
|
316
|
+
true
|
|
317
|
+
else
|
|
318
|
+
false
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
else
|
|
322
|
+
|
|
323
|
+
false
|
|
324
|
+
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ------------------------------------------------------------
|
|
331
|
+
# current node existence
|
|
332
|
+
#
|
|
333
|
+
# obfs.foo.bar._exists
|
|
334
|
+
# obfs.foo.bar._exists?
|
|
335
|
+
# ------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def _exists
|
|
338
|
+
File.exist?(@path)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def _exists?
|
|
342
|
+
_exists
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ------------------------------------------------------------
|
|
347
|
+
# direct-child existence
|
|
348
|
+
#
|
|
349
|
+
# obfs.foo._has('bar')
|
|
350
|
+
# obfs.foo._has?('bar')
|
|
351
|
+
#
|
|
352
|
+
# Unlike _exist, this uses File.exist? directly instead
|
|
353
|
+
# of enumerating the current directory.
|
|
354
|
+
# ------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def _has(term = '')
|
|
357
|
+
|
|
358
|
+
child_name =
|
|
359
|
+
validate_child_name!(
|
|
360
|
+
term
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
File.exist?(
|
|
365
|
+
File.join(
|
|
366
|
+
@path,
|
|
367
|
+
child_name
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
rescue
|
|
373
|
+
|
|
374
|
+
false
|
|
375
|
+
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _has?(term = '')
|
|
380
|
+
_has(term)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ------------------------------------------------------------
|
|
385
|
+
# current node timestamp
|
|
386
|
+
#
|
|
387
|
+
# obfs.foo._timestamp
|
|
388
|
+
#
|
|
389
|
+
# Returns nil when the node does not exist or cannot be read.
|
|
390
|
+
# ------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
def _timestamp
|
|
393
|
+
|
|
394
|
+
File.mtime(@path)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
rescue
|
|
398
|
+
|
|
399
|
+
nil
|
|
400
|
+
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ------------------------------------------------------------
|
|
405
|
+
# recursive fuzzy directory search
|
|
406
|
+
#
|
|
407
|
+
# obfs._search('nginx')
|
|
408
|
+
#
|
|
409
|
+
# obfs.systems._search(
|
|
410
|
+
# 'nginx certificate',
|
|
411
|
+
# {
|
|
412
|
+
# recursive: true,
|
|
413
|
+
# min_score: 0.6,
|
|
414
|
+
# limit: 10
|
|
415
|
+
# }
|
|
416
|
+
# )
|
|
417
|
+
#
|
|
418
|
+
# camelCase minScore is also accepted:
|
|
419
|
+
#
|
|
420
|
+
# {
|
|
421
|
+
# minScore: 0.6
|
|
422
|
+
# }
|
|
423
|
+
#
|
|
424
|
+
# Defaults:
|
|
425
|
+
#
|
|
426
|
+
# recursive: true
|
|
427
|
+
# min_score: 0.5
|
|
428
|
+
# limit: unlimited
|
|
429
|
+
#
|
|
430
|
+
# Returns:
|
|
431
|
+
#
|
|
432
|
+
# [
|
|
433
|
+
# {
|
|
434
|
+
# path: "services/nginx",
|
|
435
|
+
# score: 1.0
|
|
436
|
+
# },
|
|
437
|
+
# {
|
|
438
|
+
# path: "services/nginx/certificates",
|
|
439
|
+
# score: 0.9
|
|
440
|
+
# }
|
|
441
|
+
# ]
|
|
442
|
+
#
|
|
443
|
+
# Search:
|
|
444
|
+
#
|
|
445
|
+
# - is scoped to the current Store node
|
|
446
|
+
# - searches directories only
|
|
447
|
+
# - searches directory names and relative paths
|
|
448
|
+
# - does not read file contents
|
|
449
|
+
# - does not follow symbolic links
|
|
450
|
+
# - ignores dot-prefixed entries
|
|
451
|
+
# - returns relevance-sorted results
|
|
452
|
+
# ------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def _search(term = '', options = {})
|
|
455
|
+
|
|
456
|
+
return [] unless File.directory?(@path)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
term =
|
|
460
|
+
term
|
|
461
|
+
.to_s
|
|
462
|
+
.strip
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
return [] if term.empty?
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
options = {} unless options.is_a?(Hash)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
recursive =
|
|
472
|
+
search_option(
|
|
473
|
+
options,
|
|
474
|
+
[
|
|
475
|
+
:recursive,
|
|
476
|
+
'recursive'
|
|
477
|
+
],
|
|
478
|
+
true
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
min_score =
|
|
483
|
+
search_option(
|
|
484
|
+
options,
|
|
485
|
+
[
|
|
486
|
+
:min_score,
|
|
487
|
+
:minScore,
|
|
488
|
+
'min_score',
|
|
489
|
+
'minScore'
|
|
490
|
+
],
|
|
491
|
+
0.5
|
|
492
|
+
).to_f
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
limit =
|
|
496
|
+
search_option(
|
|
497
|
+
options,
|
|
498
|
+
[
|
|
499
|
+
:limit,
|
|
500
|
+
'limit'
|
|
501
|
+
],
|
|
502
|
+
nil
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# clamp score to 0.0 - 1.0
|
|
507
|
+
|
|
508
|
+
min_score = 0.0 if min_score < 0.0
|
|
509
|
+
min_score = 1.0 if min_score > 1.0
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
results = []
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
search_directories(
|
|
516
|
+
@path,
|
|
517
|
+
'',
|
|
518
|
+
term,
|
|
519
|
+
recursive,
|
|
520
|
+
min_score,
|
|
521
|
+
results
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
results.sort_by! do |result|
|
|
526
|
+
|
|
527
|
+
[
|
|
528
|
+
-result[:score],
|
|
529
|
+
result[:path].downcase
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
unless limit.nil? || (limit.respond_to?(:infinite?) && limit.infinite?)
|
|
536
|
+
|
|
537
|
+
begin
|
|
538
|
+
|
|
539
|
+
limit =
|
|
540
|
+
Integer(limit)
|
|
541
|
+
|
|
542
|
+
rescue
|
|
543
|
+
|
|
544
|
+
return []
|
|
545
|
+
|
|
128
546
|
end
|
|
129
547
|
|
|
548
|
+
return [] if limit <= 0
|
|
549
|
+
|
|
550
|
+
results = results.first(limit)
|
|
551
|
+
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
results
|
|
556
|
+
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
private
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ============================================================
|
|
564
|
+
# FILESYSTEM R/W
|
|
565
|
+
# ============================================================
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# ------------------------------------------------------------
|
|
569
|
+
# write
|
|
570
|
+
#
|
|
571
|
+
# Original behavior intentionally preserved.
|
|
572
|
+
#
|
|
573
|
+
# Hashes, Arrays, Strings, Numbers, Booleans, etc. continue
|
|
574
|
+
# to be JSON serialized into a single file.
|
|
575
|
+
# ------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
def write(path, filename, data)
|
|
578
|
+
|
|
579
|
+
curr_path =
|
|
580
|
+
File.join(
|
|
581
|
+
path,
|
|
582
|
+
filename
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
File.write(
|
|
587
|
+
curr_path,
|
|
588
|
+
JSON.dump(data)
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ------------------------------------------------------------
|
|
595
|
+
# read
|
|
596
|
+
#
|
|
597
|
+
# Original behavior intentionally preserved.
|
|
598
|
+
#
|
|
599
|
+
# JSON is parsed when possible.
|
|
600
|
+
# Otherwise raw file contents are returned.
|
|
601
|
+
# ------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
def read(path, filename)
|
|
604
|
+
|
|
605
|
+
curr_path =
|
|
606
|
+
File.join(
|
|
607
|
+
path,
|
|
608
|
+
filename
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
content =
|
|
613
|
+
File.open(
|
|
614
|
+
curr_path
|
|
615
|
+
).read
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
begin
|
|
619
|
+
|
|
620
|
+
JSON.parse(content)
|
|
621
|
+
|
|
622
|
+
rescue
|
|
623
|
+
|
|
624
|
+
content
|
|
625
|
+
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# ============================================================
|
|
632
|
+
# SEARCH
|
|
633
|
+
# ============================================================
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# ------------------------------------------------------------
|
|
637
|
+
# recursively search directory structure
|
|
638
|
+
# ------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
def search_directories(
|
|
641
|
+
path,
|
|
642
|
+
relative_path,
|
|
643
|
+
term,
|
|
644
|
+
recursive,
|
|
645
|
+
min_score,
|
|
646
|
+
results
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
entries =
|
|
650
|
+
Dir.entries(path).reject do |entry|
|
|
651
|
+
|
|
652
|
+
entry == '.' ||
|
|
653
|
+
entry == '..' ||
|
|
654
|
+
entry.start_with?('.')
|
|
655
|
+
|
|
656
|
+
end rescue []
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
entries.each do |entry|
|
|
660
|
+
|
|
661
|
+
full_path =
|
|
662
|
+
File.join(
|
|
663
|
+
path,
|
|
664
|
+
entry
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# do not follow symbolic links
|
|
669
|
+
|
|
670
|
+
next if File.symlink?(full_path)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
# search directory structure only
|
|
674
|
+
|
|
675
|
+
next unless File.directory?(full_path)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
child_relative_path =
|
|
679
|
+
|
|
680
|
+
if relative_path.empty?
|
|
681
|
+
|
|
682
|
+
entry
|
|
683
|
+
|
|
684
|
+
else
|
|
685
|
+
|
|
686
|
+
File.join(
|
|
687
|
+
relative_path,
|
|
688
|
+
entry
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
name_score =
|
|
695
|
+
fuzzy_score(
|
|
696
|
+
term,
|
|
697
|
+
entry
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
path_score =
|
|
702
|
+
fuzzy_score(
|
|
703
|
+
term,
|
|
704
|
+
child_relative_path
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
score =
|
|
709
|
+
[
|
|
710
|
+
name_score,
|
|
711
|
+
path_score
|
|
712
|
+
].max
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
if score >= min_score
|
|
716
|
+
|
|
717
|
+
results << {
|
|
718
|
+
path: child_relative_path,
|
|
719
|
+
score: score.round(6)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
if recursive
|
|
726
|
+
|
|
727
|
+
search_directories(
|
|
728
|
+
full_path,
|
|
729
|
+
child_relative_path,
|
|
730
|
+
term,
|
|
731
|
+
recursive,
|
|
732
|
+
min_score,
|
|
733
|
+
results
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# ------------------------------------------------------------
|
|
744
|
+
# calculate fuzzy relevance score
|
|
745
|
+
#
|
|
746
|
+
# Score range:
|
|
747
|
+
#
|
|
748
|
+
# 0.0 - 1.0
|
|
749
|
+
#
|
|
750
|
+
# Uses:
|
|
751
|
+
#
|
|
752
|
+
# - exact matches
|
|
753
|
+
# - prefix matches
|
|
754
|
+
# - substring matches
|
|
755
|
+
# - token coverage
|
|
756
|
+
# - existing Levenshtein implementation
|
|
757
|
+
# - existing WhiteSimilarity implementation
|
|
758
|
+
#
|
|
759
|
+
# No additional dependencies required.
|
|
760
|
+
# ------------------------------------------------------------
|
|
761
|
+
|
|
762
|
+
def fuzzy_score(term, candidate)
|
|
763
|
+
|
|
764
|
+
query =
|
|
765
|
+
normalize_search_term(term)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
value =
|
|
769
|
+
normalize_search_term(candidate)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
return 0.0 if query.empty? || value.empty?
|
|
773
|
+
|
|
774
|
+
return 1.0 if query == value
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
scores = []
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
# ----------------------------------------------------------
|
|
781
|
+
# prefix match
|
|
782
|
+
# ----------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
scores << 0.95 if value.start_with?(query)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# ----------------------------------------------------------
|
|
788
|
+
# substring match
|
|
789
|
+
# ----------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
scores << 0.90 if value.include?(query)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
# ----------------------------------------------------------
|
|
795
|
+
# token coverage
|
|
796
|
+
# ----------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
query_tokens =
|
|
799
|
+
query.split(/\s+/)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
unless query_tokens.empty?
|
|
803
|
+
|
|
804
|
+
matched_tokens =
|
|
805
|
+
query_tokens.count do |token|
|
|
806
|
+
|
|
807
|
+
value.include?(token)
|
|
808
|
+
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
coverage =
|
|
813
|
+
matched_tokens.to_f /
|
|
814
|
+
query_tokens.length.to_f
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
scores << (
|
|
818
|
+
coverage * 0.85
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
# ----------------------------------------------------------
|
|
825
|
+
# normalized Levenshtein similarity
|
|
826
|
+
# ----------------------------------------------------------
|
|
827
|
+
|
|
828
|
+
max_length =
|
|
829
|
+
[
|
|
830
|
+
query.length,
|
|
831
|
+
value.length
|
|
832
|
+
].max
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
if max_length > 0
|
|
836
|
+
|
|
837
|
+
distance =
|
|
838
|
+
OBFS::Levenshtein.distance(
|
|
839
|
+
query,
|
|
840
|
+
value
|
|
841
|
+
).to_f
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
levenshtein_score =
|
|
845
|
+
1.0 -
|
|
846
|
+
(
|
|
847
|
+
distance /
|
|
848
|
+
max_length.to_f
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
levenshtein_score = 0.0 if levenshtein_score < 0.0
|
|
853
|
+
levenshtein_score = 1.0 if levenshtein_score > 1.0
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
scores << levenshtein_score
|
|
857
|
+
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# ----------------------------------------------------------
|
|
862
|
+
# existing WhiteSimilarity
|
|
863
|
+
# ----------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
begin
|
|
866
|
+
|
|
867
|
+
white_score =
|
|
868
|
+
OBFS::WhiteSimilarity.similarity(
|
|
869
|
+
query,
|
|
870
|
+
value
|
|
871
|
+
).to_f
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# support either:
|
|
875
|
+
#
|
|
876
|
+
# 0.0 - 1.0
|
|
877
|
+
#
|
|
878
|
+
# or:
|
|
879
|
+
#
|
|
880
|
+
# 0 - 100
|
|
881
|
+
#
|
|
882
|
+
|
|
883
|
+
white_score /= 100.0 if white_score > 1.0
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
white_score = 0.0 if white_score < 0.0
|
|
887
|
+
white_score = 1.0 if white_score > 1.0
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
scores << white_score
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
rescue
|
|
894
|
+
|
|
895
|
+
# Remaining scorers are sufficient.
|
|
896
|
+
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
scores.empty? ? 0.0 : scores.max
|
|
901
|
+
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
# ------------------------------------------------------------
|
|
906
|
+
# normalize text for fuzzy matching
|
|
907
|
+
#
|
|
908
|
+
# Examples:
|
|
909
|
+
#
|
|
910
|
+
# "import-scenarios"
|
|
911
|
+
# =>
|
|
912
|
+
# "import scenarios"
|
|
913
|
+
#
|
|
914
|
+
# "systems/nginx_errors"
|
|
915
|
+
# =>
|
|
916
|
+
# "systems nginx errors"
|
|
917
|
+
# ------------------------------------------------------------
|
|
918
|
+
|
|
919
|
+
def normalize_search_term(value)
|
|
920
|
+
|
|
921
|
+
value
|
|
922
|
+
.to_s
|
|
923
|
+
.downcase
|
|
924
|
+
.gsub(/[\\\/:_\-]+/, ' ')
|
|
925
|
+
.gsub(/[^a-z0-9\s]/, '')
|
|
926
|
+
.gsub(/\s+/, ' ')
|
|
927
|
+
.strip
|
|
928
|
+
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
# ------------------------------------------------------------
|
|
933
|
+
# retrieve option using symbol/string equivalents
|
|
934
|
+
#
|
|
935
|
+
# Presence testing is used rather than || so:
|
|
936
|
+
#
|
|
937
|
+
# recursive: false
|
|
938
|
+
#
|
|
939
|
+
# works correctly.
|
|
940
|
+
# ------------------------------------------------------------
|
|
941
|
+
|
|
942
|
+
def search_option(
|
|
943
|
+
options,
|
|
944
|
+
keys,
|
|
945
|
+
default_value
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
keys.each do |key|
|
|
949
|
+
|
|
950
|
+
return options[key] if options.key?(key)
|
|
951
|
+
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
default_value
|
|
956
|
+
|
|
130
957
|
end
|
|
131
958
|
|
|
959
|
+
|
|
960
|
+
# ============================================================
|
|
961
|
+
# PATH SAFETY
|
|
962
|
+
# ============================================================
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
# ------------------------------------------------------------
|
|
966
|
+
# validates one OBFS object key
|
|
967
|
+
#
|
|
968
|
+
# An individual key represents exactly one filesystem node.
|
|
969
|
+
#
|
|
970
|
+
# Therefore:
|
|
971
|
+
#
|
|
972
|
+
# obfs['foo']['bar']
|
|
973
|
+
#
|
|
974
|
+
# is valid, while:
|
|
975
|
+
#
|
|
976
|
+
# obfs['foo/bar']
|
|
977
|
+
# obfs['../foo']
|
|
978
|
+
# obfs['foo/../../etc']
|
|
979
|
+
# obfs['..\\foo']
|
|
980
|
+
#
|
|
981
|
+
# are rejected.
|
|
982
|
+
#
|
|
983
|
+
# This prevents escaping the current OBFS hierarchy through
|
|
984
|
+
# bracket notation or dynamically generated keys.
|
|
985
|
+
# ------------------------------------------------------------
|
|
986
|
+
|
|
987
|
+
def validate_child_name!(name)
|
|
988
|
+
|
|
989
|
+
name =
|
|
990
|
+
name.to_s
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
if name.empty?
|
|
994
|
+
|
|
995
|
+
raise "OBFS key cannot be empty"
|
|
996
|
+
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
if name == '.' ||
|
|
1001
|
+
name == '..' ||
|
|
1002
|
+
name.include?('/') ||
|
|
1003
|
+
name.include?('\\') ||
|
|
1004
|
+
name.include?("\0")
|
|
1005
|
+
|
|
1006
|
+
raise "filesystem traversal is not allowed"
|
|
1007
|
+
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
name
|
|
1012
|
+
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
end
|
|
1016
|
+
|
|
132
1017
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: obfs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jensel Gatchalian
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: File-based, object-oriented data store for Ruby
|
|
13
13
|
email: jensel.gatchalian@gmail.com
|
|
@@ -37,7 +37,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
38
|
version: '0'
|
|
39
39
|
requirements: []
|
|
40
|
-
rubygems_version: 4.0.
|
|
40
|
+
rubygems_version: 4.0.20
|
|
41
41
|
specification_version: 4
|
|
42
42
|
summary: OBFS
|
|
43
43
|
test_files: []
|