Module: Joblin::LazyAccess
- Extended by:
- ActiveSupport::Concern
- Included in:
- LazyAccessArray, LazyAccessHash
- Defined in:
- lib/joblin/lazy_access.rb
Overview
Serialization coder (used by serialize ..., coder: Joblin::LazyAccess) that
lazily rehydrates ActiveRecord references stored as GlobalIDs.
Records are NOT auto-located from any string that happens to start with
"gid://". Instead, dump records which keys held records in a reserved
_joblin_ envelope, and load only resolves those keys — and only when the
key is actually read. This closes the object-injection/data-loss hole where
an attacker-supplied "gid://..." string was located into an arbitrary record.
Envelope shapes:
Hash with record values -> { ...values (records as gid strings)...,
"_joblin_" => { "gid_keys" => [<key>, ...] } }
A standalone record (e.g. an Array element) -> { "_joblin_" => { "gid" => "gid://..." } }
Constant Summary collapse
- MARKER =
"_joblin_".freeze
Instance Attribute Summary collapse
-
#lazy_gid_keys ⇒ Object
For a loaded LazyAccessHash: the set of keys whose values are GlobalIDs.
Class Method Summary collapse
- .dump(val) ⇒ Object
- .dump_hash(hash) ⇒ Object
-
.load(val) ⇒ Object
---- Serialization coder interface ----.
- .locate(gid_string) ⇒ Object
-
.marker_of(hash) ⇒ Object
Read the reserved marker hash regardless of string/symbol keying.
- .raw ⇒ Object
Instance Method Summary collapse
-
#[](*args) ⇒ Object
---- Lazy element access ----.
- #[]=(key, value) ⇒ Object
- #initialize ⇒ Object
Instance Attribute Details
#lazy_gid_keys ⇒ Object
For a loaded LazyAccessHash: the set of keys whose values are GlobalIDs. Tracked out-of-band so the reserved marker isn't exposed via enumeration.
27 28 29 |
# File 'lib/joblin/lazy_access.rb', line 27 def lazy_gid_keys @lazy_gid_keys end |
Class Method Details
.dump(val) ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/joblin/lazy_access.rb', line 57 def self.dump(val) case val when Array val.to_a.map { |x| dump(x) } when Hash dump_hash(val) when ActiveRecord::Base # A record outside a hash-key context (Array element / standalone): # wrap it so load can resolve it back. { MARKER => { "gid" => val.to_gid.to_s } } else val end end |
.dump_hash(hash) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/joblin/lazy_access.rb', line 72 def self.dump_hash(hash) # Keys known to hold GlobalIDs even when their value is still a raw gid # string (loaded but never accessed): a LazyAccessHash carries them on an # ivar; a raw storage hash carries them in its embedded marker. carried = if hash.respond_to?(:lazy_gid_keys) && hash.lazy_gid_keys Array(hash.lazy_gid_keys).map(&:to_s) elsif (m = marker_of(hash)) Array(m["gid_keys"]).map(&:to_s) else [] end gid_keys = [] out = {} hash.to_h.each do |k, v| ks = k.to_s next if ks == MARKER if v.is_a?(ActiveRecord::Base) out[ks] = v.to_gid.to_s gid_keys << ks elsif carried.include?(ks) && v.is_a?(String) out[ks] = v gid_keys << ks else out[ks] = dump(v) end end out[MARKER] = { "gid_keys" => gid_keys } unless gid_keys.empty? out end |
.load(val) ⇒ Object
---- Serialization coder interface ----
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/joblin/lazy_access.rb', line 39 def self.load(val) case val when Hash marker = marker_of(val) # A per-object envelope resolves to its single record reference. return locate(marker["gid"]) if marker && marker.key?("gid") gid_keys = marker ? Array(marker["gid_keys"]).map(&:to_s) : [] data = val.reject { |k, _| k.to_s == MARKER } LazyAccessHash.new(data).tap { |h| h.lazy_gid_keys = gid_keys } when Array LazyAccessArray.new(val) else val end end |
.locate(gid_string) ⇒ Object
105 106 107 108 |
# File 'lib/joblin/lazy_access.rb', line 105 def self.locate(gid_string) return gid_string unless gid_string.is_a?(String) GlobalID::Locator.locate(gid_string) end |
.marker_of(hash) ⇒ Object
Read the reserved marker hash regardless of string/symbol keying.
111 112 113 114 115 |
# File 'lib/joblin/lazy_access.rb', line 111 def self.marker_of(hash) m = hash[MARKER] m = hash[MARKER.to_sym] if m.nil? m.is_a?(Hash) ? m.with_indifferent_access : nil end |
.raw ⇒ Object
29 30 31 32 33 34 35 |
# File 'lib/joblin/lazy_access.rb', line 29 def self.raw current = Thread.current[:lazy_access_read_raw] Thread.current[:lazy_access_read_raw] = true yield ensure Thread.current[:lazy_access_read_raw] = current end |
Instance Method Details
#[](*args) ⇒ Object
---- Lazy element access ----
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/joblin/lazy_access.rb', line 119 def [](*args) # Only the single-key form is lazily resolved/cached. Multi-arg or Range # access (e.g. Array#[start, length] / arr[1..3]) delegates to super. if Thread.current[:lazy_access_read_raw] || args.length != 1 || args.first.is_a?(Range) return super end key = args.first key = key.to_s if key.is_a?(Symbol) return nil if key == MARKER unless @access_cache.key?(key) val = super @access_cache[key] = _resolve_lazy_value(key, val) end @access_cache[key] end |
#[]=(key, value) ⇒ Object
137 138 139 140 141 |
# File 'lib/joblin/lazy_access.rb', line 137 def []=(key, value) key = key.to_s if key.is_a?(Symbol) @access_cache[key] = value super end |
#initialize ⇒ Object
20 21 22 23 |
# File 'lib/joblin/lazy_access.rb', line 20 def initialize(...) super @access_cache = {} end |