obfs 0.1.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2651e95cf754b5588ce3890154db176634ee85ee3cf6bc869940b985787552e1
4
- data.tar.gz: 32aa77c08d31978cf973acbaef8636a8f0f431f69dd372ec62ae75430944b322
3
+ metadata.gz: 2a4b8b313bcfac3893da631ea24c90b5cf39f7337e4dd804e922edfa893d4d77
4
+ data.tar.gz: b7afaac2c2fa61e82e888223dd6473a14dbde609dea0eb67b06d2069bafb164a
5
5
  SHA512:
6
- metadata.gz: 21072b9fa529f96cc73fd144691344a2fa0b3383c2b55737f36f8b597924c182cb02c6c4f041a4c4d9211d95d831e2af16bfacc953a7c15971543a2ba5826a21
7
- data.tar.gz: 4de8c6e75e9a1a14aaea87095dca85296d09913fad9db69669a79e6ce4c961d27742d13e35c2c43225608ceb95ec40ba8bfd7d7674244215257b3e940098c5b4
6
+ metadata.gz: b782fbfd5d5c740c3256a74b0fb66083d102cc71c59439c673c9a86bc516a438aa63f673d7a609d55d3376fb548421d846345142429b211eeb3e3b304b0edbc6
7
+ data.tar.gz: 3fdd856e0c0c4d65b593f47893c3a85d8d2a3514a03f28656eebbece06aa5ff22a746c58f9b022b14d761187b0cd5ef7336113912caf82c06c80de4a4881fdce
data/README.md ADDED
@@ -0,0 +1,602 @@
1
+ # OBFS
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/obfs.svg)](https://badge.fury.io/rb/obfs)
4
+
5
+ File-based, object-oriented data store for Ruby.
6
+
7
+ OBFS maps recursive Ruby object access directly to the filesystem, allowing data to be addressed using dot notation, bracket notation, or a combination of both.
8
+
9
+ ```ruby
10
+ datastore.users.cat.preferences.theme = "dark"
11
+
12
+ datastore["users"]["cat"]["preferences"]["theme"]
13
+ # => "dark"
14
+ ```
15
+
16
+ The resulting filesystem structure is:
17
+
18
+ ```text
19
+ users/
20
+ └── cat/
21
+ └── preferences/
22
+ └── theme
23
+ ```
24
+
25
+ OBFS also includes filesystem discovery helpers, including direct existence checks, recursive fuzzy directory search, timestamps, and directory indexing. It uses Ruby standard libraries and independent OBFS-owned text algorithms, with no external runtime gem dependencies.
26
+
27
+
28
+ ## Quickstart
29
+
30
+ Add OBFS to your `Gemfile`:
31
+
32
+ ```ruby
33
+ gem "obfs"
34
+ ```
35
+
36
+ Require it:
37
+
38
+ ```ruby
39
+ require "obfs"
40
+ ```
41
+
42
+ Create a store:
43
+
44
+ ```ruby
45
+ datastore = OBFS::Store.new
46
+ ```
47
+
48
+ Write data:
49
+
50
+ ```ruby
51
+ datastore.somefile = "some value"
52
+
53
+ datastore["somefile"] = "some value"
54
+ ```
55
+
56
+ Read data:
57
+
58
+ ```ruby
59
+ datastore.somefile
60
+
61
+ datastore["somefile"]
62
+ ```
63
+
64
+ Delete data:
65
+
66
+ ```ruby
67
+ datastore.somefile = nil
68
+
69
+ datastore["somefile"] = nil
70
+ ```
71
+
72
+ Supported values include:
73
+
74
+ - String
75
+ - Array
76
+ - Hash
77
+ - Integer
78
+ - Float
79
+ - Boolean
80
+
81
+
82
+ ## Usage
83
+
84
+ By default, OBFS stores data inside:
85
+
86
+ ```text
87
+ ~/.obfs
88
+ ```
89
+
90
+ A custom path can be specified:
91
+
92
+ ```ruby
93
+ datastore = OBFS::Store.new({
94
+ path: "/some/other/folder"
95
+ })
96
+ ```
97
+
98
+ Paths are recursively created as needed.
99
+
100
+ For example:
101
+
102
+ ```ruby
103
+ datastore.some.long.path.to.create = "some string"
104
+ ```
105
+
106
+ creates:
107
+
108
+ ```text
109
+ some/
110
+ └── long/
111
+ └── path/
112
+ └── to/
113
+ └── create
114
+ ```
115
+
116
+ The file `create` contains the JSON-serialized value:
117
+
118
+ ```text
119
+ "some string"
120
+ ```
121
+
122
+ Bracket notation can also be used:
123
+
124
+ ```ruby
125
+ datastore["some"]["long"]["path"]["to"]["create"] = "some string"
126
+ ```
127
+
128
+ Dot and bracket notation may be mixed:
129
+
130
+ ```ruby
131
+ datastore.some["long"].path["to"].create = "some string"
132
+ ```
133
+
134
+ Hashes and arrays are serialized as JSON values into leaf files:
135
+
136
+ ```ruby
137
+ datastore.settings = {
138
+ "theme" => "dark",
139
+ "notifications" => true
140
+ }
141
+
142
+ datastore.numbers = [1, 2, 3]
143
+ ```
144
+
145
+ Reading those values returns the corresponding Ruby objects:
146
+
147
+ ```ruby
148
+ datastore.settings
149
+ # => {"theme"=>"dark", "notifications"=>true}
150
+
151
+ datastore.numbers
152
+ # => [1, 2, 3]
153
+ ```
154
+
155
+
156
+ ## Path Safety
157
+
158
+ Each OBFS key represents exactly one filesystem node.
159
+
160
+ Nested paths should therefore be expressed recursively:
161
+
162
+ ```ruby
163
+ datastore["foo"]["bar"]
164
+ ```
165
+
166
+ rather than by embedding filesystem separators:
167
+
168
+ ```ruby
169
+ datastore["foo/bar"]
170
+ ```
171
+
172
+ OBFS rejects keys containing filesystem path separators, null bytes, `.` or `..`.
173
+
174
+ For example, the following are rejected:
175
+
176
+ ```ruby
177
+ datastore[".."]
178
+ datastore["../foo"]
179
+ datastore["../../etc/passwd"]
180
+ datastore["foo/bar"]
181
+ datastore["..\\Windows"]
182
+ ```
183
+
184
+ This prevents dynamic OBFS keys from traversing outside the intended datastore hierarchy.
185
+
186
+
187
+ ## Special Methods
188
+
189
+ ### `_path`
190
+
191
+ Returns the current filesystem path.
192
+
193
+ ```ruby
194
+ datastore.some.data._path
195
+ ```
196
+
197
+ Example:
198
+
199
+ ```text
200
+ /home/user/.obfs/some/data
201
+ ```
202
+
203
+
204
+ ### `_index`
205
+
206
+ Returns the entries in the current directory.
207
+
208
+ ```ruby
209
+ datastore.some.data._index
210
+ ```
211
+
212
+ Example:
213
+
214
+ ```ruby
215
+ [
216
+ "foo",
217
+ "bar",
218
+ "baz"
219
+ ]
220
+ ```
221
+
222
+
223
+ ### `_keys`
224
+
225
+ Alias for `_index`.
226
+
227
+ ```ruby
228
+ datastore.some.data._keys
229
+ ```
230
+
231
+
232
+ ### `_find`
233
+
234
+ Performs a fuzzy search against entries in the current directory only.
235
+
236
+ ```ruby
237
+ datastore.some.data._find("some term")
238
+ ```
239
+
240
+ Full signature:
241
+
242
+ ```ruby
243
+ datastore.some.data._find(term, records, tolerance)
244
+ ```
245
+
246
+ Arguments:
247
+
248
+ - `term` - search term
249
+ - `records` - maximum number of records to return; defaults to `1000`
250
+ - `tolerance` - maximum Levenshtein distance; defaults to `50`
251
+
252
+ Results are returned as an array of entry names sorted by relevance. Entries must be within the Levenshtein tolerance and have a positive normalized bigram/Dice similarity score. The distance check is case-sensitive; similarity normalizes case, path separators, and whitespace.
253
+
254
+ Example:
255
+
256
+ ```ruby
257
+ datastore.services._find("nginx")
258
+ ```
259
+
260
+ might return:
261
+
262
+ ```ruby
263
+ [
264
+ "nginx",
265
+ "nginx_config",
266
+ "old_nginx"
267
+ ]
268
+ ```
269
+
270
+ `_find` searches only the current directory level.
271
+
272
+
273
+ ### `_exist`
274
+
275
+ Checks whether an immediate child with the specified name exists.
276
+
277
+ ```ruby
278
+ datastore.some.data._exist("some term")
279
+ ```
280
+
281
+ Returns:
282
+
283
+ ```ruby
284
+ true
285
+ ```
286
+
287
+ or:
288
+
289
+ ```ruby
290
+ false
291
+ ```
292
+
293
+ This method is retained for compatibility with earlier versions of OBFS.
294
+
295
+
296
+ ### `_exists`
297
+
298
+ Checks whether the current OBFS node physically exists.
299
+
300
+ ```ruby
301
+ datastore.some.data._exists
302
+ ```
303
+
304
+ A question-mark alias is also available:
305
+
306
+ ```ruby
307
+ datastore.some.data._exists?
308
+ ```
309
+
310
+ Example:
311
+
312
+ ```ruby
313
+ datastore.services.nginx._exists?
314
+ # => true
315
+ ```
316
+
317
+
318
+ ### `_has`
319
+
320
+ Checks whether an immediate child exists.
321
+
322
+ ```ruby
323
+ datastore.services._has("nginx")
324
+ ```
325
+
326
+ A question-mark alias is also available:
327
+
328
+ ```ruby
329
+ datastore.services._has?("nginx")
330
+ ```
331
+
332
+ Returns:
333
+
334
+ ```ruby
335
+ true
336
+ ```
337
+
338
+ or:
339
+
340
+ ```ruby
341
+ false
342
+ ```
343
+
344
+ Unlike `_exist`, `_has` performs a direct filesystem existence check rather than enumerating the current directory.
345
+
346
+
347
+ ### `_timestamp`
348
+
349
+ Returns the modification time of the current filesystem node.
350
+
351
+ ```ruby
352
+ datastore.services.nginx._timestamp
353
+ ```
354
+
355
+ Returns a Ruby `Time` object when the node exists:
356
+
357
+ ```ruby
358
+ 2026-10-04 14:30:00 -0700
359
+ ```
360
+
361
+ or:
362
+
363
+ ```ruby
364
+ nil
365
+ ```
366
+
367
+ if the node does not exist or cannot be read.
368
+
369
+
370
+ ### `_search`
371
+
372
+ Performs a fuzzy search against directory names and relative directory paths.
373
+
374
+ Unlike `_find`, `_search` can recursively search the entire directory hierarchy beneath the current OBFS node.
375
+
376
+ Basic usage:
377
+
378
+ ```ruby
379
+ datastore._search("nginx")
380
+ ```
381
+
382
+ Example result:
383
+
384
+ ```ruby
385
+ [
386
+ {
387
+ path: "systems/services/nginx",
388
+ score: 1.0
389
+ },
390
+ {
391
+ path: "systems/services/nginx/certificates",
392
+ score: 0.95
393
+ }
394
+ ]
395
+ ```
396
+
397
+ Search may also be scoped to any recursive OBFS node:
398
+
399
+ ```ruby
400
+ datastore.systems.services._search("nginx")
401
+ ```
402
+
403
+ Options can be supplied using a hash:
404
+
405
+ ```ruby
406
+ datastore._search(
407
+ "nginx certificate",
408
+ {
409
+ recursive: true,
410
+ min_score: 0.6,
411
+ limit: 10
412
+ }
413
+ )
414
+ ```
415
+
416
+ Available options:
417
+
418
+ - `recursive` - recursively search child directories; defaults to `true`
419
+ - `min_score` - minimum fuzzy relevance score from `0.0` to `1.0`; defaults to `0.5`
420
+ - `limit` - maximum number of results; unlimited by default
421
+
422
+ `minScore` is also accepted for compatibility with the JavaScript version:
423
+
424
+ ```ruby
425
+ datastore._search(
426
+ "nginx",
427
+ {
428
+ minScore: 0.6
429
+ }
430
+ )
431
+ ```
432
+
433
+ Search results are sorted from highest to lowest relevance.
434
+
435
+ `_search`:
436
+
437
+ - searches directory names
438
+ - searches relative directory paths
439
+ - does not read stored file contents
440
+ - does not follow symbolic links
441
+ - ignores dot-prefixed entries
442
+ - is scoped to the current OBFS node
443
+
444
+
445
+ ## `_find` vs `_search`
446
+
447
+ `_find` is intended for lightweight one-level discovery:
448
+
449
+ ```ruby
450
+ datastore.services._find("nginx")
451
+ ```
452
+
453
+ and returns:
454
+
455
+ ```ruby
456
+ [
457
+ "nginx",
458
+ "nginx_config"
459
+ ]
460
+ ```
461
+
462
+ `_search` is intended for deeper discovery:
463
+
464
+ ```ruby
465
+ datastore._search("nginx certificate")
466
+ ```
467
+
468
+ and returns scored relative paths:
469
+
470
+ ```ruby
471
+ [
472
+ {
473
+ path: "systems/services/nginx/certificates",
474
+ score: 0.95
475
+ }
476
+ ]
477
+ ```
478
+
479
+
480
+ ## Recursive Object Storage
481
+
482
+ One of the primary goals of OBFS is to allow the filesystem to behave like an arbitrarily recursive Ruby object.
483
+
484
+ For example:
485
+
486
+ ```ruby
487
+ datastore.projects.example.servers.production.services.nginx.status = "running"
488
+ ```
489
+
490
+ can later be accessed using the same structure:
491
+
492
+ ```ruby
493
+ datastore.projects.example.servers.production.services.nginx.status
494
+ # => "running"
495
+ ```
496
+
497
+ The hierarchy itself is stored using physical directories and files, making the datastore directly inspectable using normal filesystem tools.
498
+
499
+
500
+ ## Notes
501
+
502
+ - OBFS uses the filesystem directly as its persistence layer.
503
+ - Intermediate paths are created automatically when writing data.
504
+ - Setting a value to `nil` removes the corresponding file or directory recursively.
505
+ - Existing values are replaced when reassigned.
506
+ - JSON-compatible values are serialized using Ruby's JSON library.
507
+ - Files containing valid JSON are deserialized when read.
508
+ - Non-JSON files are returned as raw text.
509
+ - Fuzzy search uses independent `OBFS::Levenshtein` and `OBFS::StringSimilarity` implementations.
510
+ - No external runtime gem dependencies are required; the Text gem is not needed.
511
+ - No database server is required.
512
+
513
+
514
+ ## Platform Support
515
+
516
+ OBFS requires Ruby >= 2.0.0. The updated text algorithms use Ruby 2.0-compatible APIs. Syntax and basic behavior were validated on Ruby 4.0.7 on Linux; this update has not been tested on Ruby 2.0 itself.
517
+
518
+ Windows and macOS have not been formally tested.
519
+
520
+
521
+ ## Links
522
+
523
+ - [RubyGems](https://rubygems.org/gems/obfs)
524
+ - [GitHub](https://github.com/jenselg/obfs-ruby)
525
+ - [JavaScript / Node.js Version](https://github.com/jenselg/obfs)
526
+
527
+
528
+ ## Text Algorithms
529
+
530
+ The algorithms are implemented locally in `lib/obfs/levenshtein.rb` and
531
+ `lib/obfs/string_similarity.rb`. Requiring `obfs` loads both automatically.
532
+
533
+ ### `OBFS::Levenshtein.distance`
534
+
535
+ Calculates unit-cost insertion, deletion, and substitution distance over Unicode
536
+ codepoints. Inputs must support String encoding; Unicode normalization is not
537
+ performed.
538
+
539
+ ```ruby
540
+ OBFS::Levenshtein.distance("kitten", "sitting")
541
+ # => 3
542
+
543
+ OBFS::Levenshtein.distance("kitten", "sitting", 2)
544
+ # => 2
545
+ ```
546
+
547
+ The optional `max_distance` must be a nonnegative Integer or `nil`. When supplied,
548
+ the result is capped at that maximum, preserving the existing API contract. A
549
+ result equal to the maximum means the actual distance is at least that value.
550
+
551
+ ### `OBFS::StringSimilarity.similarity`
552
+
553
+ Returns a normalized character-bigram Dice score between `0.0` and `1.0`. It
554
+ normalizes case, path separators (`/`, `\`, `:`, `_`, and `-`), and whitespace.
555
+ Bigrams span the normalized string, including spaces, and repeated bigrams count
556
+ separately. Unicode codepoints are used without Unicode normalization.
557
+
558
+ ```ruby
559
+ OBFS::StringSimilarity.similarity("France", "French")
560
+ # => 0.4
561
+
562
+ OBFS::StringSimilarity.similarity("nginx_config", "NGINX CONFIG")
563
+ # => 1.0
564
+ ```
565
+
566
+ Equal nonempty normalized strings score `1.0`, including one-character strings.
567
+ Empty inputs score `0.0`; unequal one-character strings score `0.0`.
568
+
569
+ Store relevance takes the highest score from exact, prefix, substring, token
570
+ coverage, normalized Levenshtein, and Dice matching. `_find` and `_search` retain
571
+ their signatures and result formats, but rankings can change with the new Dice
572
+ scorer.
573
+
574
+ When upgrading, replace `lib/obfs/white_similarity.rb` with
575
+ `lib/obfs/string_similarity.rb` and update any direct `OBFS::WhiteSimilarity`
576
+ references to `OBFS::StringSimilarity`. The copied Text implementations and
577
+ `text/*` requires are no longer used.
578
+
579
+
580
+ ## License
581
+
582
+ ### MIT License
583
+
584
+ Copyright (c) 2021 Jensel Gatchalian <jensel.gatchalian@gmail.com>
585
+
586
+ Permission is hereby granted, free of charge, to any person obtaining a copy
587
+ of this software and associated documentation files (the "Software"), to deal
588
+ in the Software without restriction, including without limitation the rights
589
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
590
+ copies of the Software, and to permit persons to whom the Software is
591
+ furnished to do so, subject to the following conditions:
592
+
593
+ The above copyright notice and this permission notice shall be included in all
594
+ copies or substantial portions of the Software.
595
+
596
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
597
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
598
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
599
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
600
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
601
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
602
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ module OBFS
2
+ module Levenshtein
3
+ extend self
4
+
5
+ # Unit-cost edit distance over Unicode codepoints, without normalization.
6
+ # With a maximum, return the exact distance capped at that maximum.
7
+ def distance(str1, str2, max_distance = nil)
8
+ unless max_distance.nil? ||
9
+ (max_distance.is_a?(Integer) && max_distance >= 0)
10
+ raise ArgumentError, 'max_distance must be a nonnegative Integer or nil'
11
+ end
12
+ return 0 if max_distance == 0
13
+
14
+ left = str1.encode('UTF-8').each_codepoint.to_a
15
+ right = str2.encode('UTF-8').each_codepoint.to_a
16
+ return 0 if left == right
17
+
18
+ # The shorter input determines the width of the two working rows.
19
+ left, right = right, left if left.length < right.length
20
+ if max_distance && left.length - right.length >= max_distance
21
+ return max_distance
22
+ end
23
+ return left.length if right.empty?
24
+
25
+ previous = (0..right.length).to_a
26
+ left.each_with_index do |character, row|
27
+ current = [row + 1]
28
+ right.each_with_index do |other, column|
29
+ current << [current[column] + 1,
30
+ previous[column + 1] + 1,
31
+ previous[column] + (character == other ? 0 : 1)].min
32
+ end
33
+ # Every path to the final cell crosses this row; costs cannot decrease.
34
+ return max_distance if max_distance && current.min >= max_distance
35
+ previous = current
36
+ end
37
+
38
+ max_distance ? [previous.last, max_distance].min : previous.last
39
+ end
40
+ end
41
+ end