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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/obfs/store.rb +999 -114
  3. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2651e95cf754b5588ce3890154db176634ee85ee3cf6bc869940b985787552e1
4
- data.tar.gz: 32aa77c08d31978cf973acbaef8636a8f0f431f69dd372ec62ae75430944b322
3
+ metadata.gz: 648385c40ccd4ea5cb5f608fc7164e1c86757f5ec6c2053711ebf71eab7fa752
4
+ data.tar.gz: f89b3c016e0949fab62abf7187970db6e16524e927dfa5a98747a6e3d6e7bc83
5
5
  SHA512:
6
- metadata.gz: 21072b9fa529f96cc73fd144691344a2fa0b3383c2b55737f36f8b597924c182cb02c6c4f041a4c4d9211d95d831e2af16bfacc953a7c15971543a2ba5826a21
7
- data.tar.gz: 4de8c6e75e9a1a14aaea87095dca85296d09913fad9db69669a79e6ce4c961d27742d13e35c2c43225608ceb95ec40ba8bfd7d7674244215257b3e940098c5b4
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
- class Store
6
+ class Store
4
7
 
5
- def initialize(attributes = {}) # hash argument
6
- @path = (attributes.keys.include? :path) ? attributes[:path] : (File.join(Dir.home, '.obfs'))
7
- end
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
- # special methods
13
+ # ============================================================
14
+ # REGULAR METHODS
15
+ # ============================================================
79
16
 
80
- # returns current working path for obfs
81
- def _path
82
- @path
83
- end
17
+ def method_missing(m, *args, &block)
84
18
 
85
- # returns directory contents in an array
86
- def _index
87
- Dir.entries(@path).reject { |k| k == '.' || k == '..' } rescue nil
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
- # searches directory contents (1 level) and returns boolean if term exist
103
- def _exist(term = '')
104
- exist_space = Dir.entries(@path).reject { |k| k != term.to_s || k == '.' || k == '..' } rescue nil
105
- if !exist_space.nil?
106
- if exist_space.length > 0
107
- true
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
- private
31
+ if method_name.end_with?('=')
117
32
 
118
- # filesystem R/W
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
- def read(path, filename)
126
- curr_path = File.join path, filename
127
- JSON.parse(File.open(curr_path).read) rescue File.open(curr_path).read
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.1.2
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: 2021-01-16 00:00:00.000000000 Z
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.18
40
+ rubygems_version: 4.0.20
41
41
  specification_version: 4
42
42
  summary: OBFS
43
43
  test_files: []