png_conform 0.1.2 → 0.1.3

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +116 -6
  3. data/Gemfile +1 -1
  4. data/config/validation_profiles.yml +105 -0
  5. data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
  6. data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
  7. data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
  8. data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
  9. data/lib/png_conform/cli.rb +12 -0
  10. data/lib/png_conform/commands/check_command.rb +118 -53
  11. data/lib/png_conform/configuration.rb +147 -0
  12. data/lib/png_conform/container.rb +113 -0
  13. data/lib/png_conform/models/validation_result.rb +30 -4
  14. data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
  15. data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
  16. data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
  17. data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
  18. data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
  19. data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
  20. data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
  21. data/lib/png_conform/readers/full_load_reader.rb +13 -4
  22. data/lib/png_conform/readers/streaming_reader.rb +27 -2
  23. data/lib/png_conform/reporters/color_reporter.rb +17 -14
  24. data/lib/png_conform/reporters/visual_elements.rb +22 -16
  25. data/lib/png_conform/services/analysis_manager.rb +120 -0
  26. data/lib/png_conform/services/chunk_processor.rb +195 -0
  27. data/lib/png_conform/services/file_signature.rb +226 -0
  28. data/lib/png_conform/services/file_strategy.rb +78 -0
  29. data/lib/png_conform/services/lru_cache.rb +170 -0
  30. data/lib/png_conform/services/parallel_validator.rb +118 -0
  31. data/lib/png_conform/services/profile_manager.rb +41 -12
  32. data/lib/png_conform/services/result_builder.rb +299 -0
  33. data/lib/png_conform/services/validation_cache.rb +210 -0
  34. data/lib/png_conform/services/validation_orchestrator.rb +188 -0
  35. data/lib/png_conform/services/validation_service.rb +53 -337
  36. data/lib/png_conform/services/validator_pool.rb +142 -0
  37. data/lib/png_conform/utils/colorizer.rb +149 -0
  38. data/lib/png_conform/validators/chunk_registry.rb +12 -0
  39. data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
  40. data/lib/png_conform/version.rb +1 -1
  41. data/png_conform.gemspec +1 -0
  42. metadata +38 -2
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lru_cache"
4
+ require "digest"
5
+
6
+ module PngConform
7
+ module Services
8
+ # Validation cache for storing and reusing validation results
9
+ #
10
+ # Provides multi-level caching for validation operations to avoid
11
+ # redundant I/O and computation. Supports LRU eviction for memory efficiency.
12
+ #
13
+ class ValidationCache
14
+ attr_reader :file_cache, :max_files, :enabled
15
+
16
+ # Default configuration
17
+ DEFAULT_MAX_FILES = 100
18
+ DEFAULT_ENABLED = true
19
+
20
+ class << self
21
+ # Get global singleton instance
22
+ #
23
+ # @return [ValidationCache] Global cache instance
24
+ def instance
25
+ @instance ||= new
26
+ end
27
+
28
+ # Reset global cache
29
+ #
30
+ # @return [void]
31
+ def reset!
32
+ @instance = new
33
+ end
34
+ end
35
+
36
+ # Initialize validation cache
37
+ #
38
+ # @param options [Hash] Cache configuration options
39
+ def initialize(options = {})
40
+ @max_files = options.fetch(:max_files, DEFAULT_MAX_FILES)
41
+ @file_cache = LRUCache.new(@max_files)
42
+ @enabled = options.fetch(:enabled, DEFAULT_ENABLED)
43
+ @stats = {
44
+ hits: 0,
45
+ misses: 0,
46
+ evictions: 0,
47
+ }
48
+ end
49
+
50
+ # Fetch cached validation result for a file
51
+ #
52
+ # @param file_path [String] Path to PNG file
53
+ # @param options [Hash] Validation options (affects cache key)
54
+ # @return [FileAnalysis, nil] Cached result or nil if not cached
55
+ def fetch(file_path, options = {})
56
+ return nil unless @enabled
57
+
58
+ cache_key = generate_cache_key(file_path, options)
59
+ entry = @file_cache[cache_key]
60
+
61
+ if entry
62
+ @stats[:hits] += 1
63
+ # Return shallow copy - FileAnalysis objects are read-only after creation
64
+ # This is significantly faster than Marshal deep copy
65
+ shallow_copy(entry[:result])
66
+ else
67
+ @stats[:misses] += 1
68
+ nil
69
+ end
70
+ end
71
+
72
+ # Store validation result in cache
73
+ #
74
+ # @param file_path [String] Path to PNG file
75
+ # @param options [Hash] Validation options (affects cache key)
76
+ # @param result [FileAnalysis] Validation result to cache
77
+ # @return [void]
78
+ def store(file_path, options, result)
79
+ return unless @enabled
80
+
81
+ cache_key = generate_cache_key(file_path, options)
82
+ entry = {
83
+ result: result, # Store reference - objects are read-only
84
+ timestamp: Time.now,
85
+ file_size: result.file_size,
86
+ validation_time: result.validation_result&.validation_time,
87
+ }
88
+
89
+ @file_cache[cache_key] = entry
90
+ end
91
+
92
+ # Check if file validation is cached
93
+ #
94
+ # @param file_path [String] Path to PNG file
95
+ # @param options [Hash] Validation options
96
+ # @return [Boolean] True if result is cached
97
+ def cached?(file_path, options = {})
98
+ return false unless @enabled
99
+
100
+ cache_key = generate_cache_key(file_path, options)
101
+ @file_cache.key?(cache_key)
102
+ end
103
+
104
+ # Invalidate cache entry for a file
105
+ #
106
+ # @param file_path [String] Path to PNG file
107
+ # @param options [Hash] Validation options (optional)
108
+ # @return [Boolean] True if entry was found and removed
109
+ def invalidate(file_path, options = {})
110
+ return false unless @enabled
111
+
112
+ cache_key = generate_cache_key(file_path, options)
113
+ deleted = @file_cache.delete(cache_key)
114
+
115
+ if deleted
116
+ @stats[:evictions] += 1
117
+ end
118
+
119
+ !!deleted
120
+ end
121
+
122
+ # Clear all cached entries
123
+ #
124
+ # @return [void]
125
+ def clear
126
+ @file_cache.clear
127
+ reset_stats
128
+ end
129
+
130
+ # Get cache statistics
131
+ #
132
+ # @return [Hash] Cache performance statistics
133
+ def stats
134
+ total_requests = @stats[:hits] + @stats[:misses]
135
+ hit_rate = if total_requests.positive?
136
+ (@stats[:hits].to_f / total_requests * 100).round(1)
137
+ else
138
+ 0.0
139
+ end
140
+
141
+ {
142
+ enabled: @enabled,
143
+ max_files: @max_files,
144
+ current_files: @file_cache.current_size,
145
+ hits: @stats[:hits],
146
+ misses: @stats[:misses],
147
+ evictions: @stats[:evictions],
148
+ hit_rate: hit_rate,
149
+ cache_stats: @file_cache.stats,
150
+ }
151
+ end
152
+
153
+ # Reset statistics counters
154
+ #
155
+ # @return [void]
156
+ def reset_stats
157
+ @stats = {
158
+ hits: 0,
159
+ misses: 0,
160
+ evictions: 0,
161
+ }
162
+ end
163
+
164
+ # Get cache key for file validation
165
+ #
166
+ # The cache key includes file path, mtime, and relevant options
167
+ # to ensure cache validity when files change or options differ.
168
+ #
169
+ # @param file_path [String] Path to PNG file
170
+ # @param options [Hash] Validation options
171
+ # @return [String] Cache key
172
+ def generate_cache_key(file_path, options = {})
173
+ # Include relevant options in cache key
174
+ relevant_opts = options.slice(:profile, :batch_enabled, :fail_fast)
175
+
176
+ # Get file modification time for cache invalidation
177
+ mtime = begin
178
+ File.mtime(file_path).to_i
179
+ rescue StandardError
180
+ 0
181
+ end
182
+
183
+ # Create deterministic cache key
184
+ key_parts = [
185
+ file_path,
186
+ mtime,
187
+ relevant_opts.sort.to_h,
188
+ ]
189
+
190
+ Digest::SHA256.hexdigest(key_parts.flatten.join(":"))
191
+ end
192
+
193
+ private
194
+
195
+ # Create shallow copy of result object
196
+ #
197
+ # Uses .dup for fast shallow copying. FileAnalysis objects are
198
+ # effectively read-only after creation, so deep copying is unnecessary.
199
+ #
200
+ # @param obj [Object] Object to copy
201
+ # @return [Object] Shallow copy of object
202
+ def shallow_copy(obj)
203
+ obj.dup
204
+ rescue TypeError
205
+ # For objects that can't be dup'd, return as-is
206
+ obj
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chunk_processor"
4
+ require_relative "result_builder"
5
+ require_relative "analysis_manager"
6
+
7
+ module PngConform
8
+ module Services
9
+ # Main validation orchestration service
10
+ #
11
+ # This service coordinates the validation of PNG files by:
12
+ # 1. Validating the PNG signature
13
+ # 2. Processing chunks through ChunkProcessor
14
+ # 3. Validating chunk sequence requirements
15
+ # 4. Building the complete FileAnalysis result
16
+ #
17
+ # The orchestrator follows a pipeline architecture:
18
+ # File → Signature → Chunks → Sequence → Analysis → Result
19
+ #
20
+ class ValidationOrchestrator
21
+ attr_reader :reader, :filepath, :options, :context, :chunks
22
+
23
+ # Convenience method to validate a file by path
24
+ #
25
+ # @param filepath [String] Path to PNG file
26
+ # @param options [Hash] Optional CLI options
27
+ # @return [FileAnalysis] Complete file analysis
28
+ def self.validate_file(filepath, options = {})
29
+ require_relative "../readers/full_load_reader"
30
+ reader = Readers::FullLoadReader.new(filepath)
31
+ new(reader, filepath, options).validate
32
+ end
33
+
34
+ # Initialize validation orchestrator
35
+ #
36
+ # @param reader [Object] File reader (StreamingReader or FullLoadReader)
37
+ # @param filepath [String, nil] Optional file path (for reporting)
38
+ # @param options [Hash] CLI options for controlling behavior
39
+ def initialize(reader, filepath = nil, options = {})
40
+ @reader = reader
41
+ @filepath = filepath
42
+ @options = options
43
+ @context = Validators::ValidationContext.new
44
+ @chunks = []
45
+ end
46
+
47
+ # Validate the PNG file
48
+ #
49
+ # This is the main entry point for validation. It orchestrates
50
+ # all validation stages in the correct order.
51
+ #
52
+ # @return [FileAnalysis] Complete file analysis with all data
53
+ def validate
54
+ validate_signature
55
+ process_chunks
56
+ validate_sequence
57
+ build_result
58
+ end
59
+
60
+ # Get all errors from the validation context
61
+ #
62
+ # @return [Array<Hash>] Array of error hashes
63
+ def errors
64
+ @context.all_errors
65
+ end
66
+
67
+ # Get all warnings from the validation context
68
+ #
69
+ # @return [Array<Hash>] Array of warning hashes
70
+ def warnings
71
+ @context.all_warnings
72
+ end
73
+
74
+ # Get all info messages from the validation context
75
+ #
76
+ # @return [Array<Hash>] Array of info hashes
77
+ def info_messages
78
+ @context.all_info
79
+ end
80
+
81
+ private
82
+
83
+ # Validate PNG signature
84
+ #
85
+ # Checks that the file starts with the PNG signature:
86
+ # 137 80 78 71 13 10 26 10
87
+ #
88
+ # @return [void]
89
+ def validate_signature
90
+ sig = reader.signature
91
+ expected = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
92
+
93
+ return if sig == expected
94
+
95
+ @context.add_error(
96
+ chunk_type: "SIGNATURE",
97
+ message: "Invalid PNG signature",
98
+ severity: :error,
99
+ )
100
+ end
101
+
102
+ # Process all chunks using ChunkProcessor
103
+ #
104
+ # Delegates chunk validation to the ChunkProcessor which handles:
105
+ # - Iterating through chunks
106
+ # - Creating validators via ChunkRegistry
107
+ # - Collecting validation results
108
+ #
109
+ # @return [void]
110
+ def process_chunks
111
+ processor = ChunkProcessor.new(@reader, @context, @options)
112
+ processor.process do |chunk|
113
+ @chunks << chunk
114
+ end
115
+ end
116
+
117
+ # Validate chunk sequence requirements
118
+ #
119
+ # Checks high-level sequencing rules:
120
+ # - IHDR must be first chunk
121
+ # - IEND must be last chunk
122
+ # - At least one IDAT chunk required
123
+ #
124
+ # @return [void]
125
+ def validate_sequence
126
+ validate_ihdr_first
127
+ validate_iend_last
128
+ validate_idat_present
129
+ end
130
+
131
+ # Check that IHDR is the first chunk
132
+ #
133
+ # @return [void]
134
+ def validate_ihdr_first
135
+ return if @context.seen?("IHDR")
136
+
137
+ @context.add_error(
138
+ chunk_type: "IHDR",
139
+ message: "Missing IHDR chunk (must be first)",
140
+ severity: :error,
141
+ )
142
+ end
143
+
144
+ # Check that IEND is the last chunk
145
+ #
146
+ # @return [void]
147
+ def validate_iend_last
148
+ return if @context.seen?("IEND")
149
+
150
+ @context.add_error(
151
+ chunk_type: "IEND",
152
+ message: "Missing IEND chunk (must be last)",
153
+ severity: :error,
154
+ )
155
+ end
156
+
157
+ # Check that at least one IDAT chunk exists
158
+ #
159
+ # @return [void]
160
+ def validate_idat_present
161
+ return if @context.seen?("IDAT")
162
+
163
+ @context.add_error(
164
+ chunk_type: "IDAT",
165
+ message: "Missing IDAT chunk (at least one required)",
166
+ severity: :error,
167
+ )
168
+ end
169
+
170
+ # Build complete FileAnalysis with validation results and analyzer data
171
+ #
172
+ # Coordinates ResultBuilder and AnalysisManager to produce the final result
173
+ #
174
+ # @return [FileAnalysis] Complete analysis model
175
+ def build_result
176
+ result_builder = ResultBuilder.new(@reader, @filepath, @context,
177
+ @chunks, @options)
178
+ file_analysis = result_builder.build
179
+
180
+ # Run analyzers through AnalysisManager
181
+ analysis_manager = AnalysisManager.new(@options)
182
+ analysis_manager.enrich(file_analysis)
183
+
184
+ file_analysis
185
+ end
186
+ end
187
+ end
188
+ end