camada 0.2.0 → 0.2.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 +4 -4
- data/README.md +1 -3
- data/lib/camada/constants.rb +2 -2
- data/lib/camada/snapshot/parse.rb +66 -17
- data/lib/camada/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b73c94df7715601ab82c6a470ccd976e6bb934f6dcf6c8c54c5bfc3fe00b97df
|
|
4
|
+
data.tar.gz: 94bd0d19b9259b6e03ae0e6df41ca717afbd9aa915d7b1f273c99b89b3dab425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b6a3f8d9b8185f0979dccfb92498bc6fac1b62fdb1221f0130f69ca606edf5783ef25df6414e11997a13bb1a1dc5b18b874b5e3b570bef8e980230626c55fa6
|
|
7
|
+
data.tar.gz: 0e879615dbc999eed321ca8bc7821498622ee91a0be08345c846508a9cbd74aea4eab42f55eb7468fbc27482d593c190fa8e94b66c19e7482731dc3cf5159100
|
data/README.md
CHANGED
|
@@ -170,9 +170,7 @@ valid `_cch`, then `nil`. A non-page (non-HTML) request also gets `nil` and is s
|
|
|
170
170
|
`sdk-ruby` is an in-app tap: status, latency, session, the beacon's browser signals and your
|
|
171
171
|
outcomes. Puma hands the env to Rack as binary strings; the middleware reads them as UTF-8 with
|
|
172
172
|
invalid bytes replaced (U+FFFD) before matching and shipping, so a stray high byte in a header
|
|
173
|
-
neither escapes a rule nor costs the batch it rides in. The Rack env carries no wire header order (`hord` is the env's order), so
|
|
174
|
-
reads no HEADER_ORDER signal from this tap, and it never scores the absence of header order, ASN,
|
|
175
|
-
country or a TLS fingerprint against a request; ASN and country it resolves itself. Enforcement
|
|
173
|
+
neither escapes a rule nor costs the batch it rides in. The Rack env carries no wire header order (`hord` is the env's order), so this tap ships no header-order signal; camada resolves ASN and country itself. Enforcement
|
|
176
174
|
at this position covers ip, path, user-agent and header conditions — ASN, country and TLS entries
|
|
177
175
|
fail open in-app. `matches` patterns are JS regexes read by Ruby's Onigmo: named groups are
|
|
178
176
|
native, `[^]` and `\cX` are translated, `^`/`$` become `\A`/`\z` (Ruby's are line anchors,
|
data/lib/camada/constants.rb
CHANGED
|
@@ -4,8 +4,8 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Camada
|
|
6
6
|
# Tap identifier this SDK claims on the wire. The server validates against its own enum and
|
|
7
|
-
#
|
|
8
|
-
# itself
|
|
7
|
+
# decides itself what each tap can see: an SDK can never grant
|
|
8
|
+
# itself more than that, only name its position — and an unknown name is silently read as a
|
|
9
9
|
# proxy, so this literal is load-bearing.
|
|
10
10
|
TAP = "sdk-ruby"
|
|
11
11
|
|
|
@@ -149,9 +149,10 @@ module Camada
|
|
|
149
149
|
|
|
150
150
|
# A pattern this runtime rejects never matches, and never throws (fail open). Patterns are
|
|
151
151
|
# authored as JS regexes (the analyst validates them with `new RegExp`), so the JS spellings
|
|
152
|
-
# Onigmo reads differently are translated first — see js_to_onigmo; \d \w
|
|
153
|
-
# Ruby by default, as JS reads them
|
|
154
|
-
#
|
|
152
|
+
# Onigmo reads differently are translated first — see js_to_onigmo; \d \w are ASCII in
|
|
153
|
+
# Ruby by default, as JS reads them, and \b \B are emitted as (?a:\b) for the same reason.
|
|
154
|
+
# An astral char is one code point here and two UTF-16 units in JS,
|
|
155
|
+
# so `.`, `[^a]` and `\S` count it once (an accepted gap, camada-all-vuvo).
|
|
155
156
|
# A quantifier right after a raw astral char repeats the whole char here but only its low unit in JS,
|
|
156
157
|
# so js_to_onigmo answers as V8 does: the char when the count allows 1, else `(?!)` (camada-all-5lra, see astral_count).
|
|
157
158
|
# Where a lookbehind ends up holding `(?!)`, Onigmo cannot compile it, so compile_regex returns nil and the rule never matches.
|
|
@@ -163,7 +164,8 @@ module Camada
|
|
|
163
164
|
# "ff", and an ASCII-only pattern does the same once Ruby recompiles it for a non-ASCII value. Extra matches only, so a rule hits a
|
|
164
165
|
# value JS would not and never misses one; splitting every literal run would slow string search (an accepted gap, camada-all-wdii).
|
|
165
166
|
# Inside a lookbehind js_to_onigmo splits adjacent literals (camada-all-ccjo) so it stays fixed-length, which stops ss folding to ß,
|
|
166
|
-
#
|
|
167
|
+
# and reads a multi-fold char (ß, ff, İ) case-sensitively, as JS matches it: Onigmo there reads it as its fold alone (ß as "ss",
|
|
168
|
+
# never "ß"; Ruby 3.3.13 and 3.4.12 do the same for ff and İ, camada-all-ozix).
|
|
167
169
|
# A backreference JS and Onigmo read differently is rewritten or, when it cannot be, fails open (scan_groups).
|
|
168
170
|
# A `(?` or `(*` group JS lacks (`(?i)`, `(?>`, `(*FAIL)`, ...) fails open too: JS rejects it or V8 mis-evaluates it (camada-all-lsl8).
|
|
169
171
|
# Only a UTF-8 or ASCII pattern is walked: snapshot patterns arrive as UTF-8 JSON, and the binary walk
|
|
@@ -366,9 +368,12 @@ module Camada
|
|
|
366
368
|
# Under IGNORECASE Onigmo folds several chars at once (`ss` ~ ß, and a positive class holding
|
|
367
369
|
# `\S` gains such folds), which makes a lookbehind variable-length and uncompilable; JS folds one
|
|
368
370
|
# char at a time. So inside a lookbehind adjacent literal chars are split with `(?:)` (`ab` -> `a(?:)b`; a
|
|
369
|
-
# lone literal stays a plain string node
|
|
371
|
+
# lone literal stays a plain string node) and a positive
|
|
370
372
|
# class with `\S` (or `\W`/`\D`) becomes `(?:[^WS]|[rest])`, and a closed one-char class counts as a literal for
|
|
371
|
-
# that split (camada-all-ccjo, camada-all-fe1r).
|
|
373
|
+
# that split (camada-all-ccjo, camada-all-fe1r). A char whose fold is several chars (ß, ff, İ; raw, `\u00df` or `\ß`)
|
|
374
|
+
# becomes `(?-i:ß)` there, pulled out of a positive class like `\S` unless a `-` is beside it (camada-all-ozix):
|
|
375
|
+
# JS /i matches it only to itself, and Onigmo would read it as its fold alone (version-dependent).
|
|
376
|
+
# One of the 63 iota-subscript chars in a positive class is pulled out the same way in any context (camada-all-rmje).
|
|
372
377
|
# In a class `\c` is a control char only before a letter, digit or `_` (ClassControlLetter); before
|
|
373
378
|
# anything else (`[\c*]`) it is a literal `\` then `c` (camada-all-mqcb).
|
|
374
379
|
# A `{m,n}` bound above Onigmo's 100000 is clamped: m to 100000, and an n above it drops (`{m,}`,
|
|
@@ -395,11 +400,26 @@ module Camada
|
|
|
395
400
|
bars = [-1] # per open group, the root first: the index of the last `|` in it or an ancestor
|
|
396
401
|
lbs = [false] # per open group, the root first: inside a lookbehind (camada-all-ccjo)
|
|
397
402
|
lit_end = -1 # the pattern index right after the last literal emitted inside a lookbehind
|
|
398
|
-
class_start = nil # the out index of an open positive class's `[`
|
|
399
|
-
class_alts = [] # the `\S`/`\W`/`\D` that class held
|
|
403
|
+
class_start = nil # the out index of an open positive class's `[`
|
|
404
|
+
class_alts = [] # the `\S`/`\W`/`\D` and multi-fold chars that class held
|
|
400
405
|
last_named = {} # name -> the index of its last opener
|
|
401
406
|
while i < n
|
|
402
407
|
ch = pattern[i]
|
|
408
|
+
w = multi_fold(pattern, i)
|
|
409
|
+
iota = w && IOTA_SUBSCRIPT.match?(w[2])
|
|
410
|
+
if !in_class && !lbs.last && iota
|
|
411
|
+
out << "(?-i:#{w[0]})" # outside a lookbehind (the branch below) and a class; JS /i matches it only to itself (camada-all-zn38)
|
|
412
|
+
i += w[1]
|
|
413
|
+
next
|
|
414
|
+
end
|
|
415
|
+
# in a lookbehind (positive class too, unless a range end) a multi-fold char becomes `(?-i:c)` (camada-all-ozix)
|
|
416
|
+
# an iota-subscript char in a positive class is pulled out the same way, in a lookbehind or not (camada-all-rmje)
|
|
417
|
+
if (lbs.last || (in_class && iota)) && (!in_class || class_start) && w &&
|
|
418
|
+
!(in_class && (pattern[i - 1] == "-" || pattern[i + w[1]] == "-"))
|
|
419
|
+
in_class ? class_alts << "(?-i:#{w[0]})" : out << "(?-i:#{w[0]})"
|
|
420
|
+
i += w[1]
|
|
421
|
+
next
|
|
422
|
+
end
|
|
403
423
|
if ch == "\\" && i + 1 < n
|
|
404
424
|
if !in_class && (r = plan[i])
|
|
405
425
|
if r[1] == :empty
|
|
@@ -413,7 +433,7 @@ module Camada
|
|
|
413
433
|
next
|
|
414
434
|
end
|
|
415
435
|
l = pattern[i + 1]
|
|
416
|
-
if in_class && class_start && (l == "S" || l == "W" || l == "D")
|
|
436
|
+
if in_class && class_start && lbs.last && (l == "S" || l == "W" || l == "D")
|
|
417
437
|
class_alts << (l == "S" ? JS_SPACE["S"] : "\\#{l}") # spliced in at the `]`: a positive class holding one gains multi-char folds
|
|
418
438
|
pull_end = i + 2
|
|
419
439
|
i += 2
|
|
@@ -425,6 +445,11 @@ module Camada
|
|
|
425
445
|
i += 2
|
|
426
446
|
next
|
|
427
447
|
end
|
|
448
|
+
if !in_class && (l == "b" || l == "B")
|
|
449
|
+
out << "(?a:\\#{l})" # Onigmo's \b reads é as a word char, JS (no u flag) as ASCII only
|
|
450
|
+
i += 2
|
|
451
|
+
next
|
|
452
|
+
end
|
|
428
453
|
if l == "c" && pattern[i + 2]&.match?(in_class ? /[A-Za-z0-9_]/ : /[A-Za-z]/) # JS Annex B: `[\c1]` too
|
|
429
454
|
out << Regexp.escape((pattern[i + 2].ord & 0x1F).chr)
|
|
430
455
|
i += 3
|
|
@@ -447,12 +472,13 @@ module Camada
|
|
|
447
472
|
i += 1
|
|
448
473
|
next
|
|
449
474
|
end
|
|
450
|
-
kept = !l.match?(/[A-Za-z]/) || pattern.match?(JS_ESCAPE, i + 1) ||
|
|
475
|
+
kept = !l.match?(/[A-Za-z]/) || pattern.match?(JS_ESCAPE, i + 1) ||
|
|
451
476
|
(l == "k" && named)
|
|
452
477
|
l = "\\\\c" if l == "c" # JS reads `\c` before a non-letter as text
|
|
453
478
|
if kept
|
|
454
479
|
e = (pattern.match(/\G\\\d+/n, i) || pattern.match(JS_ATOM, i))[0] # the whole escape: a lookbehind must not split `\x41`
|
|
455
|
-
|
|
480
|
+
# JS reads \xHH above 0x7F as that code point, Onigmo as a raw byte (camada-all-w2w4)
|
|
481
|
+
out << (e.match?(/\A\\x[89a-fA-F]\h\z/) ? "\\u00#{e[2..]}" : e)
|
|
456
482
|
i += e.length
|
|
457
483
|
next
|
|
458
484
|
end
|
|
@@ -477,12 +503,12 @@ module Camada
|
|
|
477
503
|
else
|
|
478
504
|
out << "]"
|
|
479
505
|
end
|
|
480
|
-
lit_end = i + 1 if class_start # Onigmo merges a one-char class into the literals beside it
|
|
506
|
+
lit_end = i + 1 if class_start && lbs.last # Onigmo merges a one-char class into the literals beside it
|
|
481
507
|
class_start = nil
|
|
482
508
|
i += 1
|
|
483
509
|
next
|
|
484
510
|
end
|
|
485
|
-
if ch == "-" && (pull_end == i || solo_end == i || pattern.match?(class_start ? /\G-\\[sSWD]/ : /\G-\\[sS]/, i))
|
|
511
|
+
if ch == "-" && (pull_end == i || solo_end == i || pattern.match?(class_start && lbs.last ? /\G-\\[sSWD]/ : /\G-\\[sS]/, i))
|
|
486
512
|
if pull_end == i # JS reads the atom after that dash as standalone: a dash right after it is text too
|
|
487
513
|
m = pattern.match(JS_ATOM, i + 1) # nil when the pattern ends on the dash
|
|
488
514
|
solo_end = m ? i + 1 + m[0].length : pattern.length
|
|
@@ -539,10 +565,8 @@ module Camada
|
|
|
539
565
|
i = j + 1
|
|
540
566
|
next
|
|
541
567
|
end
|
|
542
|
-
if !neg && lbs.last
|
|
543
|
-
|
|
544
|
-
class_start = out.length
|
|
545
|
-
end
|
|
568
|
+
out << "(?:)" if !neg && lbs.last && lit_end == i
|
|
569
|
+
class_start = out.length unless neg
|
|
546
570
|
out << pattern[i...j]
|
|
547
571
|
i = j
|
|
548
572
|
in_class = true
|
|
@@ -614,6 +638,31 @@ module Camada
|
|
|
614
638
|
str.encoding == Encoding::BINARY ? str : str.b
|
|
615
639
|
end
|
|
616
640
|
|
|
641
|
+
# The 63 Greek chars whose fold ends in an iota (ᾀ -> ἀι): Onigmo never matches one before a lookaround under /i.
|
|
642
|
+
IOTA_SUBSCRIPT = /\A[\u1F80-\u1FAF\u1FB2-\u1FB4\u1FB7\u1FBC\u1FC2-\u1FC4\u1FC7\u1FCC\u1FF2-\u1FF4\u1FF7\u1FFC]/
|
|
643
|
+
|
|
644
|
+
# [its text, its length, the char] when the char at i (raw UTF-8, `\uXXXX` or an identity escape `\ß`) has a case fold of
|
|
645
|
+
# several chars (ß, İ, ff: 104 of them), else nil. JS /i without `u` matches each of them only to itself.
|
|
646
|
+
def self.multi_fold(pattern, i)
|
|
647
|
+
if pattern[i] == "\\"
|
|
648
|
+
return (w = multi_fold(pattern, i + 1)) && [w[0], w[1] + 1, w[2]] if pattern.getbyte(i + 1).to_i >= 0x80
|
|
649
|
+
|
|
650
|
+
m = pattern.match(/\G\\(?:u(\h{4})|x([89a-fA-F]\h))/n, i) # `\xHH` above 0x7F is the code point (camada-all-w2w4)
|
|
651
|
+
return nil unless m && !(cp = (m[1] || m[2]).hex).between?(0xD800, 0xDFFF)
|
|
652
|
+
|
|
653
|
+
c = cp.chr(Encoding::UTF_8)
|
|
654
|
+
len = m[0].length
|
|
655
|
+
return [format("\\u%04x", cp), len] if c.downcase(:fold).length > 1 && m[2]
|
|
656
|
+
else
|
|
657
|
+
b = pattern.getbyte(i)
|
|
658
|
+
return nil unless b.between?(0xC2, 0xEF) # a 2- or 3-byte lead: every such char is in the BMP
|
|
659
|
+
|
|
660
|
+
len = b < 0xE0 ? 2 : 3
|
|
661
|
+
c = pattern.byteslice(i, len).force_encoding(Encoding::UTF_8)
|
|
662
|
+
end
|
|
663
|
+
[pattern.byteslice(i, len), len, c] if c.valid_encoding? && c.downcase(:fold).length > 1
|
|
664
|
+
end
|
|
665
|
+
|
|
617
666
|
def self.brace_quantifier(match)
|
|
618
667
|
lo, hi = canonical_bounds(match[1], match[3])
|
|
619
668
|
return match[0] unless lo # m > n is a JS SyntaxError: leave it for Onigmo to reject
|
data/lib/camada/version.rb
CHANGED
|
@@ -6,6 +6,6 @@ module Camada
|
|
|
6
6
|
# camada-web, camada-mkt) parse this file with /^\s*VERSION = "([^"]+)"/m the way they parse
|
|
7
7
|
# a package.json. Plain X.Y.Z only, and @camada/ruby must stay in the analyst's SDK_NAMES:
|
|
8
8
|
# its sdkId drops anything else.
|
|
9
|
-
VERSION = "0.2.
|
|
9
|
+
VERSION = "0.2.2"
|
|
10
10
|
SDK_ID = "@camada/ruby/#{VERSION}".freeze
|
|
11
11
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: camada
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- camada
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'camada SDK for Ruby: inline enforcement of your snapshot (ordered custom
|
|
14
14
|
rules, then allow, block, challenge), first-party beacon and proof-of-work challenge,
|