compliance_engine 0.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbedee4752b0b7fd84e25194dd8fca02ce5bcf95fe9e36cadae328d82914da63
4
- data.tar.gz: 53536b5e5f5d53ba4d6d7a7679bee7f1b4256e57d7877562f2a336c53588d3ba
3
+ metadata.gz: fce58aedae595351e68af63ff5af15c884bb0b8449c3ec4e2ebb71ebbad873d9
4
+ data.tar.gz: cf5c54d0cde492c74df83bfd7701779c8db1a79a289ca34825f868faacffab03
5
5
  SHA512:
6
- metadata.gz: 15a7b6c0d11717ff5b0da2f89fd14207bda9f6a245a0e424a63f0693f8add5aa48e7798911b1058071f024ce45cebc5ce24266302c3dd22c4db402fb7dece58e
7
- data.tar.gz: 9b4354ce4d0bc54fd2284f6c9ff082eb135d1e342e6889673341ff1f66965ad60de504d53638abb05bd4cf7b09d23649e6cf56024da4639068a5f1e3e0b64065
6
+ metadata.gz: 35511b511226cdd62a750d3d39e8f40cc9edece3fc72cc310356b05da62679b6ca241ee4d60f88eef731e4ce252627436288414b95f32f3125ce2918f9d9a61b
7
+ data.tar.gz: fef7c63498f348f9d92d2c61e35d00e59822a59cfad779244d948b10c744a026eb304a23ff40da90d26142a710a775f0370697755e790563f6ba8736cf218ec3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ### 0.2.2 / 2026-03-02
2
+ * Ensure that cloned/duped objects get independent collection instances
3
+
1
4
  ### 0.2.1 / 2026-02-12
2
5
  * Turn off debugging in compliance_engine::enforcement function
3
6
 
@@ -32,6 +32,27 @@ class ComplianceEngine::Collection
32
32
  nil
33
33
  end
34
34
 
35
+ # Ensure that cloned/duped objects get independent component instances.
36
+ #
37
+ # Ruby's default clone/dup is a shallow copy, so @collection would be
38
+ # shared between the source and the copy, and every Component inside it
39
+ # would be the same object. Calling invalidate_cache on one copy would
40
+ # then propagate its facts into those shared components, silently
41
+ # affecting the other copy's view of the data.
42
+ #
43
+ # Duping each Component (which triggers Component#initialize_copy) gives
44
+ # every copy of the Collection its own independent components. Any
45
+ # derived caches on the Collection itself (e.g. @by_oval_id in Ces) are
46
+ # cleared so each copy rebuilds them from its own component set.
47
+ #
48
+ # @return [NilClass]
49
+ def initialize_copy(_source)
50
+ super
51
+ @collection = @collection.transform_values(&:dup)
52
+ (instance_variables - (context_variables + [:@collection])).each { |var| instance_variable_set(var, nil) }
53
+ nil
54
+ end
55
+
35
56
  # Converts the object to a hash representation
36
57
  #
37
58
  # @return [Hash] the hash representation of the object
@@ -26,6 +26,29 @@ class ComplianceEngine::Component
26
26
  nil
27
27
  end
28
28
 
29
+ # Ensure that cloned/duped objects get independent fragment stores.
30
+ #
31
+ # Ruby's default clone/dup is a shallow copy, so @component (and the
32
+ # fragments hash nested within it) would be shared between the source
33
+ # and the copy. Calling add on either would write into the same
34
+ # fragments hash, making new fragments visible on both objects.
35
+ # Pre-computed @element and @fragments caches would also be shared,
36
+ # returning stale fact-filtered results on the copy even after facts
37
+ # are changed.
38
+ #
39
+ # Duping @component and its inner fragments hash ensures each copy has
40
+ # an independent fragment store. Cache variables are cleared so each
41
+ # copy rebuilds them lazily from its own fragments on first access.
42
+ #
43
+ # @return [NilClass]
44
+ def initialize_copy(_source)
45
+ super
46
+ @component = @component.dup
47
+ @component[:fragments] = @component[:fragments].transform_values { |fragment| Marshal.load(Marshal.dump(fragment)) }
48
+ cache_variables.each { |var| instance_variable_set(var, nil) }
49
+ nil
50
+ end
51
+
29
52
  # Adds a value to the fragments array of the component.
30
53
  #
31
54
  # @param value [Object] The value to be added to the fragments array.
@@ -250,7 +273,7 @@ class ComplianceEngine::Component
250
273
  @element = {}
251
274
 
252
275
  fragments.each_value do |fragment|
253
- @element = DeepMerge.deep_merge!(fragment, @element)
276
+ @element = DeepMerge.deep_merge!(Marshal.load(Marshal.dump(fragment)), @element)
254
277
  end
255
278
 
256
279
  @element
@@ -88,6 +88,42 @@ class ComplianceEngine::Data
88
88
  (instance_variables - (data_variables + context_variables)).each { |var| instance_variable_set(var, nil) }
89
89
  end
90
90
 
91
+ # Ensure that cloned/duped objects get independent collection instances.
92
+ #
93
+ # Ruby's default clone/dup is a shallow copy, so the collection instance
94
+ # variables (@ces, @profiles, @checks, @controls) would otherwise point to
95
+ # the same objects as the source. When facts= is later called on either the
96
+ # source or the clone, invalidate_cache propagates facts into the shared
97
+ # collection, causing the other object to silently adopt the wrong facts.
98
+ #
99
+ # Nilling the collection variables here forces each clone to lazily rebuild
100
+ # its own collections the first time they are accessed, using its own context
101
+ # (facts, enforcement_tolerance, etc.). Cache variables that depend on those
102
+ # collections are cleared for the same reason.
103
+ #
104
+ # @return [NilClass]
105
+ def initialize_copy(_source)
106
+ super
107
+ # Give each clone its own outer @data hash and its own per-file inner
108
+ # hashes so that new files opened on one clone (via open/update) are not
109
+ # visible to other clones or the source, and so that a loader refresh on
110
+ # the source (which mutates the inner hash in-place via Data#update) does
111
+ # not silently affect a clone that has not yet built its lazy collections.
112
+ # The inner per-file content values (read-only parsed data) stay shared.
113
+ #
114
+ # :loader is additionally cleared (set to nil) so the copy does not hold
115
+ # a reference to the source's DataLoader object. If it did, the copy
116
+ # calling update(key_string) for an already-known file would invoke
117
+ # loader.refresh, which notifies the source (the registered Observable
118
+ # observer) and overwrites source.data[key][:content] while the copy's
119
+ # inner hash stays stale. With :loader nil the copy creates its own
120
+ # independent loader (and registers itself as observer) on next access.
121
+ @data = @data.transform_values { |entry| entry.merge(loader: nil) }
122
+ collection_variables.each { |var| instance_variable_set(var, nil) }
123
+ cache_variables.each { |var| instance_variable_set(var, nil) }
124
+ nil
125
+ end
126
+
91
127
  # Scan a Puppet environment from a zip file
92
128
  # @param path [String] The Puppet environment archive file
93
129
  # @return [NilClass]
@@ -191,8 +227,13 @@ class ComplianceEngine::Data
191
227
  else
192
228
  data[filename.key] ||= {}
193
229
 
194
- # Assume filename is a loader object
195
- unless data[filename.key]&.key?(:loader)
230
+ # Register as an observer only when no loader is currently attached.
231
+ # Checking the :loader value (rather than key presence) is important
232
+ # after clone/dup: initialize_copy sets :loader to nil so the copy does
233
+ # not share the source's loader, but the key still exists. Checking
234
+ # key presence would see the nil as "already registered" and skip
235
+ # add_observer, leaving the copy deaf to future loader refreshes.
236
+ unless data[filename.key][:loader]
196
237
  data[filename.key][:loader] = filename
197
238
  data[filename.key][:loader].add_observer(self, :update)
198
239
  end
@@ -20,13 +20,17 @@ class ComplianceEngine::DataLoader
20
20
 
21
21
  # Set the data for the data loader
22
22
  #
23
- # @param value [Hash] The new value for the data loader
23
+ # The hash and all nested hashes, arrays, and strings within it are
24
+ # deep-frozen so that parsed compliance data is treated as read-only
25
+ # once loaded. Callers must not retain a mutable reference to the
26
+ # hash after calling this method.
24
27
  #
28
+ # @param value [Hash] The new value for the data loader
25
29
  # @raise [ComplianceEngine::Error] If the value is not a Hash
26
30
  def data=(value)
27
31
  raise ComplianceEngine::Error, 'Data must be a hash' unless value.is_a?(Hash)
28
32
 
29
- @data = value
33
+ @data = deep_freeze(value)
30
34
  changed
31
35
  notify_observers(self)
32
36
  end
@@ -43,4 +47,24 @@ class ComplianceEngine::DataLoader
43
47
  require 'securerandom'
44
48
  @key = "#{data.class}:#{SecureRandom.uuid}"
45
49
  end
50
+
51
+ private
52
+
53
+ # Recursively freezes a Hash or Array and all nested objects.
54
+ #
55
+ # Parsed compliance data is read-only once loaded; deep-freezing it makes
56
+ # that invariant explicit and surfaces any accidental in-place mutation
57
+ # immediately as a FrozenError rather than silent data corruption.
58
+ #
59
+ # @param obj [Object] the object to freeze
60
+ # @return [Object] the frozen object (modified in-place)
61
+ def deep_freeze(obj)
62
+ case obj
63
+ when Hash
64
+ obj.each_value { |v| deep_freeze(v) }
65
+ when Array
66
+ obj.each { |v| deep_freeze(v) }
67
+ end
68
+ obj.freeze
69
+ end
46
70
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ComplianceEngine
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
 
6
6
  # Handle supported compliance data versions
7
7
  class Version
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compliance_engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steven Pritchard
@@ -153,7 +153,7 @@ licenses:
153
153
  metadata:
154
154
  homepage_uri: https://simp-project.com/docs/sce/
155
155
  source_code_uri: https://github.com/simp/rubygem-simp-compliance_engine
156
- changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.2.1
156
+ changelog_uri: https://github.com/simp/rubygem-simp-compliance_engine/releases/tag/0.2.2
157
157
  bug_tracker_uri: https://github.com/simp/rubygem-simp-compliance_engine/issues
158
158
  rdoc_options: []
159
159
  require_paths: