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

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#lazy_gid_keysObject

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

.rawObject



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

#initializeObject



20
21
22
23
# File 'lib/joblin/lazy_access.rb', line 20

def initialize(...)
  super
  @access_cache = {}
end