png_conform 0.1.1 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +82 -42
  3. data/Gemfile +2 -0
  4. data/README.adoc +3 -2
  5. data/benchmarks/README.adoc +570 -0
  6. data/benchmarks/config/default.yml +35 -0
  7. data/benchmarks/config/full.yml +32 -0
  8. data/benchmarks/config/quick.yml +32 -0
  9. data/benchmarks/direct_validation.rb +18 -0
  10. data/benchmarks/lib/benchmark_runner.rb +204 -0
  11. data/benchmarks/lib/metrics_collector.rb +193 -0
  12. data/benchmarks/lib/png_conform_runner.rb +68 -0
  13. data/benchmarks/lib/pngcheck_runner.rb +67 -0
  14. data/benchmarks/lib/report_generator.rb +301 -0
  15. data/benchmarks/lib/tool_runner.rb +104 -0
  16. data/benchmarks/profile_loading.rb +12 -0
  17. data/benchmarks/profile_validation.rb +18 -0
  18. data/benchmarks/results/.gitkeep +0 -0
  19. data/benchmarks/run_benchmark.rb +159 -0
  20. data/config/validation_profiles.yml +105 -0
  21. data/docs/CHUNK_TYPES.adoc +42 -0
  22. data/examples/README.md +282 -0
  23. data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
  24. data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
  25. data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
  26. data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
  27. data/lib/png_conform/cli.rb +12 -0
  28. data/lib/png_conform/commands/check_command.rb +118 -52
  29. data/lib/png_conform/configuration.rb +147 -0
  30. data/lib/png_conform/container.rb +113 -0
  31. data/lib/png_conform/models/decoded_chunk_data.rb +33 -0
  32. data/lib/png_conform/models/validation_result.rb +30 -4
  33. data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
  34. data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
  35. data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
  36. data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
  37. data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
  38. data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
  39. data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
  40. data/lib/png_conform/readers/full_load_reader.rb +13 -4
  41. data/lib/png_conform/readers/streaming_reader.rb +27 -2
  42. data/lib/png_conform/reporters/color_reporter.rb +17 -14
  43. data/lib/png_conform/reporters/reporter_factory.rb +18 -11
  44. data/lib/png_conform/reporters/visual_elements.rb +22 -16
  45. data/lib/png_conform/services/analysis_manager.rb +120 -0
  46. data/lib/png_conform/services/chunk_processor.rb +195 -0
  47. data/lib/png_conform/services/file_signature.rb +226 -0
  48. data/lib/png_conform/services/file_strategy.rb +78 -0
  49. data/lib/png_conform/services/lru_cache.rb +170 -0
  50. data/lib/png_conform/services/parallel_validator.rb +118 -0
  51. data/lib/png_conform/services/profile_manager.rb +41 -12
  52. data/lib/png_conform/services/result_builder.rb +299 -0
  53. data/lib/png_conform/services/validation_cache.rb +210 -0
  54. data/lib/png_conform/services/validation_orchestrator.rb +188 -0
  55. data/lib/png_conform/services/validation_service.rb +82 -321
  56. data/lib/png_conform/services/validator_pool.rb +142 -0
  57. data/lib/png_conform/utils/colorizer.rb +149 -0
  58. data/lib/png_conform/validators/ancillary/idot_validator.rb +102 -0
  59. data/lib/png_conform/validators/chunk_registry.rb +143 -128
  60. data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
  61. data/lib/png_conform/version.rb +1 -1
  62. data/lib/png_conform.rb +7 -46
  63. data/png_conform.gemspec +1 -0
  64. metadata +55 -2
@@ -27,15 +27,19 @@ module PngConform
27
27
  .pack("C*")
28
28
  .freeze
29
29
 
30
- attr_reader :io, :signature
30
+ attr_reader :io, :signature, :total_bytes_read
31
31
 
32
32
  # Initialize a new streaming reader
33
33
  #
34
34
  # @param io [IO] IO object to read from (must be opened in binary mode)
35
- def initialize(io)
35
+ # @param options [Hash] Options for reading behavior
36
+ # @option options [Boolean] :validate_crc (true) Calculate CRC during reading
37
+ def initialize(io, options = {})
36
38
  @io = io
37
39
  @signature = nil
38
40
  @chunks_read = 0
41
+ @total_bytes_read = 0
42
+ @validate_crc = options.fetch(:validate_crc, true)
39
43
  end
40
44
 
41
45
  # Read and validate the PNG signature
@@ -71,6 +75,15 @@ module PngConform
71
75
  return nil if @io.eof?
72
76
 
73
77
  chunk = BinData::ChunkStructure.read(@io)
78
+
79
+ # Track total bytes read (8 byte header + data + 4 byte CRC)
80
+ @total_bytes_read += (12 + chunk.data_length)
81
+
82
+ # Validate CRC during reading if enabled and cache result
83
+ if @validate_crc
84
+ chunk.instance_variable_set(:@_crc_valid, chunk.crc_valid?)
85
+ end
86
+
74
87
  @chunks_read += 1
75
88
  chunk
76
89
  rescue EOFError
@@ -135,6 +148,18 @@ module PngConform
135
148
  @io.rewind
136
149
  @signature = nil
137
150
  @chunks_read = 0
151
+ @total_bytes_read = 0
152
+ end
153
+
154
+ # Get file size from total bytes tracked during reading
155
+ #
156
+ # Returns the total file size including signature and all chunks.
157
+ # This is cached during reading to avoid O(n) recalculation.
158
+ #
159
+ # @return [Integer] File size in bytes (0 if no chunks read yet)
160
+ def file_size
161
+ # 8 bytes signature + total chunk bytes
162
+ 8 + @total_bytes_read
138
163
  end
139
164
 
140
165
  # Get the current position in the file
@@ -1,23 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_reporter"
4
+ require_relative "../utils/colorizer"
4
5
 
5
6
  module PngConform
6
7
  module Reporters
7
8
  # Color reporter - wraps another reporter to add ANSI color support (-c flag)
8
9
  # Decorator pattern to add color to any reporter
9
10
  class ColorReporter < BaseReporter
10
- # ANSI color codes
11
- COLORS = {
12
- red: "\e[31m",
13
- green: "\e[32m",
14
- yellow: "\e[33m",
15
- blue: "\e[34m",
16
- magenta: "\e[35m",
17
- cyan: "\e[36m",
18
- reset: "\e[0m",
19
- }.freeze
20
-
21
11
  attr_reader :wrapped_reporter
22
12
 
23
13
  # @param wrapped_reporter [BaseReporter] The reporter to wrap with colors
@@ -41,12 +31,25 @@ module PngConform
41
31
 
42
32
  protected
43
33
 
44
- # Override colorize to actually apply colors
34
+ # Override colorize to use Colorizer class
45
35
  def colorize(text, color)
46
36
  return text unless @colorize_enabled
47
- return text unless COLORS.key?(color)
48
37
 
49
- "#{COLORS[color]}#{text}#{COLORS[:reset]}"
38
+ case color
39
+ when :red
40
+ Utils::Colorizer.error(text, bold: false)
41
+ when :green
42
+ Utils::Colorizer.success(text, bold: false)
43
+ when :yellow
44
+ Utils::Colorizer.warning(text, bold: false)
45
+ when :blue
46
+ Utils::Colorizer.info(text, bold: false)
47
+ when :cyan, :magenta
48
+ # Use cyan as a neutral color for other cases
49
+ Utils::Colorizer.colorize(text, :cyan, bold: false)
50
+ else
51
+ text
52
+ end
50
53
  end
51
54
 
52
55
  # Determine color based on validation status
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "summary_reporter"
4
- require_relative "verbose_reporter"
5
- require_relative "very_verbose_reporter"
6
- require_relative "quiet_reporter"
7
- require_relative "palette_reporter"
8
- require_relative "text_reporter"
9
- require_relative "color_reporter"
10
- require_relative "yaml_reporter"
11
- require_relative "json_reporter"
12
-
13
3
  module PngConform
14
4
  module Reporters
15
5
  # Factory for creating reporter instances based on options.
16
6
  #
17
7
  # Implements the Factory pattern to provide a clean interface for
18
8
  # creating reporters with various combinations of options.
9
+ # Uses lazy loading to only require reporters when actually needed.
19
10
  class ReporterFactory
20
11
  # Create a reporter based on the specified options.
21
12
  #
@@ -34,30 +25,43 @@ module PngConform
34
25
  # Format takes priority over verbosity
35
26
  case format
36
27
  when "yaml"
28
+ require_relative "yaml_reporter" unless defined?(YamlReporter)
37
29
  return YamlReporter.new
38
30
  when "json"
31
+ require_relative "json_reporter" unless defined?(JsonReporter)
39
32
  return JsonReporter.new
40
33
  end
41
34
 
42
- # Text reporters with verbosity levels
35
+ # Text reporters with verbosity levels - lazy load base and visual elements
36
+ require_relative "visual_elements" unless defined?(VisualElements)
37
+ require_relative "base_reporter" unless defined?(BaseReporter)
38
+
43
39
  reporter = if verbosity
44
40
  case verbosity
45
41
  when :quiet
42
+ require_relative "quiet_reporter" unless defined?(QuietReporter)
46
43
  QuietReporter.new($stdout, colorize: colorize)
47
44
  when :verbose
45
+ require_relative "verbose_reporter" unless defined?(VerboseReporter)
48
46
  VerboseReporter.new($stdout, colorize: colorize)
49
47
  when :very_verbose
48
+ require_relative "very_verbose_reporter" unless defined?(VeryVerboseReporter)
50
49
  VeryVerboseReporter.new($stdout, colorize: colorize)
51
50
  when :summary
51
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
52
52
  SummaryReporter.new($stdout, colorize: colorize)
53
53
  else
54
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
54
55
  SummaryReporter.new($stdout, colorize: colorize)
55
56
  end
56
57
  elsif quiet
58
+ require_relative "quiet_reporter" unless defined?(QuietReporter)
57
59
  QuietReporter.new($stdout, colorize: colorize)
58
60
  elsif verbose
61
+ require_relative "verbose_reporter" unless defined?(VerboseReporter)
59
62
  VerboseReporter.new($stdout, colorize: colorize)
60
63
  else
64
+ require_relative "summary_reporter" unless defined?(SummaryReporter)
61
65
  SummaryReporter.new($stdout, colorize: colorize)
62
66
  end
63
67
 
@@ -76,6 +80,7 @@ module PngConform
76
80
  # @param reporter [BaseReporter] Reporter to wrap
77
81
  # @return [PaletteReporter] Wrapped reporter
78
82
  def self.wrap_with_palette(reporter)
83
+ require_relative "palette_reporter" unless defined?(PaletteReporter)
79
84
  PaletteReporter.new(reporter)
80
85
  end
81
86
 
@@ -86,6 +91,7 @@ module PngConform
86
91
  # @param escape_mode [Symbol] Explicit escape mode setting
87
92
  # @return [TextReporter] Wrapped reporter
88
93
  def self.wrap_with_text(reporter, seven_bit, escape_mode)
94
+ require_relative "text_reporter" unless defined?(TextReporter)
89
95
  mode = if escape_mode == :none
90
96
  (seven_bit ? :seven_bit : :none)
91
97
  else
@@ -99,6 +105,7 @@ module PngConform
99
105
  # @param reporter [BaseReporter] Reporter to wrap
100
106
  # @return [ColorReporter] Wrapped reporter
101
107
  def self.wrap_with_color(reporter)
108
+ require_relative "color_reporter" unless defined?(ColorReporter)
102
109
  ColorReporter.new(reporter)
103
110
  end
104
111
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../utils/colorizer"
4
+
3
5
  module PngConform
4
6
  module Reporters
5
7
  # Module providing visual elements (emojis and colors) for CLI output
@@ -20,27 +22,31 @@ module PngConform
20
22
  image: "🖼️",
21
23
  }.freeze
22
24
 
23
- # ANSI color codes for terminal output
24
- COLORS = {
25
- green: "\e[32m",
26
- red: "\e[31m",
27
- yellow: "\e[33m",
28
- blue: "\e[34m",
29
- cyan: "\e[36m",
30
- gray: "\e[90m",
31
- reset: "\e[0m",
32
- bold: "\e[1m",
33
- }.freeze
34
-
35
- # Colorize text with ANSI color codes
25
+ # Colorize text with Colorizer class
36
26
  # @param text [String] The text to colorize
37
- # @param color [Symbol] The color name from COLORS
27
+ # @param color [Symbol] The color name
38
28
  # @return [String] Colorized text or original if colorization disabled
39
29
  def colorize(text, color)
40
30
  return text unless @colorize
41
- return text unless COLORS.key?(color)
42
31
 
43
- "#{COLORS[color]}#{text}#{COLORS[:reset]}"
32
+ case color
33
+ when :green
34
+ Utils::Colorizer.success(text, bold: false)
35
+ when :red
36
+ Utils::Colorizer.error(text, bold: false)
37
+ when :yellow
38
+ Utils::Colorizer.warning(text, bold: false)
39
+ when :blue
40
+ Utils::Colorizer.info(text, bold: false)
41
+ when :cyan
42
+ Utils::Colorizer.colorize(text, :cyan, bold: false)
43
+ when :gray
44
+ Utils::Colorizer.colorize(text, :gray, bold: false)
45
+ when :bold
46
+ Utils::Colorizer.bold(text)
47
+ else
48
+ text
49
+ end
44
50
  end
45
51
 
46
52
  # Get emoji for a given name
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../configuration"
4
+
5
+ module PngConform
6
+ module Services
7
+ # Manages conditional analysis execution
8
+ #
9
+ # The AnalysisManager handles:
10
+ # - Running resolution analysis (conditional)
11
+ # - Running optimization analysis (conditional)
12
+ # - Running metrics analysis (conditional)
13
+ #
14
+ # Analyzers are only run when needed based on options to improve performance.
15
+ # This class extracts analysis logic from ValidationService following
16
+ # Single Responsibility Principle.
17
+ #
18
+ class AnalysisManager
19
+ # Initialize analysis manager
20
+ #
21
+ # @param options [Hash] CLI options for controlling behavior
22
+ # @param config [Configuration] Configuration instance (optional)
23
+ def initialize(options = {}, config: Configuration.instance)
24
+ @options = options
25
+ @config = config
26
+ end
27
+
28
+ # Enrich FileAnalysis with conditional analyzer results
29
+ #
30
+ # Runs analyzers only when needed based on options:
31
+ # - Resolution analysis: unless quiet mode, or if --resolution or --mobile-ready
32
+ # - Optimization analysis: unless quiet mode, or if --optimize
33
+ # - Metrics analysis: if yaml/json format, or if --metrics
34
+ #
35
+ # @param file_analysis [FileAnalysis] File analysis to enrich
36
+ # @return [FileAnalysis] Enriched file analysis
37
+ def enrich(file_analysis)
38
+ validation_result = file_analysis.validation_result
39
+
40
+ if need_resolution_analysis?
41
+ file_analysis.resolution_analysis =
42
+ run_resolution_analysis(validation_result)
43
+ end
44
+
45
+ if need_optimization_analysis?
46
+ file_analysis.optimization_analysis =
47
+ run_optimization_analysis(validation_result)
48
+ end
49
+
50
+ if need_metrics_analysis?
51
+ file_analysis.metrics = run_metrics_analysis(validation_result)
52
+ end
53
+
54
+ file_analysis
55
+ end
56
+
57
+ private
58
+
59
+ # Run resolution analyzer
60
+ #
61
+ # @param result [ValidationResult] Validation result
62
+ # @return [Hash] Resolution analysis results
63
+ def run_resolution_analysis(result)
64
+ require_relative "../analyzers/resolution_analyzer" unless defined?(Analyzers::ResolutionAnalyzer)
65
+ Analyzers::ResolutionAnalyzer.new(result, config: @config).analyze
66
+ rescue StandardError => e
67
+ { error: "Resolution analysis failed: #{e.message}" }
68
+ end
69
+
70
+ # Run optimization analyzer
71
+ #
72
+ # @param result [ValidationResult] Validation result
73
+ # @return [Hash] Optimization analysis results
74
+ def run_optimization_analysis(result)
75
+ require_relative "../analyzers/optimization_analyzer" unless defined?(Analyzers::OptimizationAnalyzer)
76
+ Analyzers::OptimizationAnalyzer.new(result, config: @config).analyze
77
+ rescue StandardError => e
78
+ { error: "Optimization analysis failed: #{e.message}" }
79
+ end
80
+
81
+ # Run metrics analyzer
82
+ #
83
+ # @param result [ValidationResult] Validation result
84
+ # @return [Hash] Metrics analysis results
85
+ def run_metrics_analysis(result)
86
+ require_relative "../analyzers/metrics_analyzer" unless defined?(Analyzers::MetricsAnalyzer)
87
+ Analyzers::MetricsAnalyzer.new(result, config: @config).analyze
88
+ rescue StandardError => e
89
+ { error: "Metrics analysis failed: #{e.message}" }
90
+ end
91
+
92
+ # Check if resolution analysis is needed
93
+ #
94
+ # @return [Boolean] True if resolution analysis should be run
95
+ def need_resolution_analysis?
96
+ return true unless @options[:quiet]
97
+
98
+ @options[:resolution] || @options[:mobile_ready]
99
+ end
100
+
101
+ # Check if optimization analysis is needed
102
+ #
103
+ # @return [Boolean] True if optimization analysis should be run
104
+ def need_optimization_analysis?
105
+ return true unless @options[:quiet]
106
+
107
+ @options[:optimize]
108
+ end
109
+
110
+ # Check if metrics analysis is needed
111
+ #
112
+ # @return [Boolean] True if metrics analysis should be run
113
+ def need_metrics_analysis?
114
+ return true if ["yaml", "json"].include?(@options[:format])
115
+
116
+ @options[:metrics]
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validators/chunk_registry"
4
+ require_relative "validator_pool"
5
+
6
+ module PngConform
7
+ module Services
8
+ # Processes chunks through validation pipeline
9
+ #
10
+ # The ChunkProcessor handles:
11
+ # - Iterating through chunks from the reader
12
+ # - Creating validators via ChunkRegistry
13
+ # - Collecting validation results
14
+ # - Handling unknown chunk types
15
+ #
16
+ # This class extracts chunk processing logic from ValidationService
17
+ # following Single Responsibility Principle.
18
+ #
19
+ class ChunkProcessor
20
+ # Initialize chunk processor
21
+ #
22
+ # @param reader [Object] File reader (StreamingReader or FullLoadReader)
23
+ # @param context [ValidationContext] Validation context for state
24
+ # @param options [Hash] CLI options for controlling behavior
25
+ def initialize(reader, context, options = {})
26
+ @reader = reader
27
+ @context = context
28
+ @options = options
29
+ @validator_pool = ValidatorPool.new(options.slice(:max_per_type))
30
+ end
31
+
32
+ # Process all chunks from the reader
33
+ #
34
+ # Uses batch validation by default for performance (groups chunks by type).
35
+ # Falls back to individual processing if batch_disabled option is set.
36
+ # Supports early termination if fail_fast option is enabled.
37
+ #
38
+ # @yield [chunk] Optional block to receive chunks as they're processed
39
+ # @return [void]
40
+ def process(&block)
41
+ # Use batch validation by default (faster for files with many chunks)
42
+ if @options[:batch_enabled] == false
43
+ process_individual(&block)
44
+ else
45
+ process_batch_inline(&block)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Process chunks in batch inline (collecting from reader)
52
+ #
53
+ # Performance optimization: groups chunks by type and validates
54
+ # in batches to reduce validator instantiation overhead.
55
+ #
56
+ # @return [void]
57
+ def process_batch_inline
58
+ # Collect all chunks first
59
+ chunk_groups = Hash.new { |h, k| h[k] = [] }
60
+
61
+ @reader.each_chunk do |chunk|
62
+ chunk_groups[chunk.chunk_type.to_s] << chunk
63
+ yield chunk if block_given?
64
+ end
65
+
66
+ # Validate each group together
67
+ chunk_groups.each do |chunk_type, chunks|
68
+ validate_chunk_batch(chunk_type, chunks)
69
+
70
+ # Early termination check after each batch
71
+ break if @options[:fail_fast] && @context.has_errors?
72
+ end
73
+ end
74
+
75
+ # Process chunks individually (original behavior)
76
+ #
77
+ # @return [void]
78
+ def process_individual
79
+ @reader.each_chunk do |chunk|
80
+ validate_chunk(chunk)
81
+ yield chunk if block_given?
82
+
83
+ break if @options[:fail_fast] && @context.has_errors?
84
+ end
85
+ end
86
+
87
+ # Process chunks in batch (performance optimization)
88
+ #
89
+ # Groups chunks by type and validates in batches to reduce
90
+ # validator instantiation overhead.
91
+ #
92
+ # @param chunks [Array] Array of chunks to validate
93
+ # @return [void]
94
+ def process_batch(chunks)
95
+ grouped = chunks.group_by { |c| c.chunk_type.to_s }
96
+
97
+ grouped.each do |chunk_type, chunk_group|
98
+ validate_chunk_batch(chunk_type, chunk_group)
99
+ end
100
+ end
101
+
102
+ # Validate a single chunk
103
+ #
104
+ # Gets validator from pool for this chunk type from ChunkRegistry,
105
+ # executes validation, and handles unknown chunks.
106
+ #
107
+ # @param chunk [Object] Chunk to validate
108
+ # @return [void]
109
+ def validate_chunk(chunk)
110
+ chunk_type = chunk.chunk_type.to_s
111
+ validator_class = Validators::ChunkRegistry.validator_for(chunk_type)
112
+
113
+ if validator_class
114
+ validator = @validator_pool.acquire(chunk_type, validator_class,
115
+ chunk, @context)
116
+ begin
117
+ validator.validate
118
+ ensure
119
+ @validator_pool.release(chunk_type, validator)
120
+ end
121
+ else
122
+ handle_unknown_chunk(chunk)
123
+ end
124
+
125
+ # Mark chunk as seen AFTER validation
126
+ # This allows validators to check for duplicates before marking
127
+ @context.mark_chunk_seen(chunk_type, chunk)
128
+ end
129
+
130
+ # Validate a batch of chunks of the same type
131
+ #
132
+ # Performance optimization: reduces validator instantiation
133
+ # overhead by pooling validators for multiple chunks of same type.
134
+ #
135
+ # @param chunk_type [String] Type of chunks in batch
136
+ # @param chunks [Array] Array of chunks of same type
137
+ # @return [void]
138
+ def validate_chunk_batch(chunk_type, chunks)
139
+ validator_class = Validators::ChunkRegistry.validator_for(chunk_type)
140
+
141
+ unless validator_class
142
+ # No validator for this chunk type - handle as unknown
143
+ chunks.each do |chunk|
144
+ handle_unknown_chunk(chunk)
145
+ @context.mark_chunk_seen(chunk.chunk_type.to_s, chunk)
146
+ end
147
+ return
148
+ end
149
+
150
+ chunks.each do |chunk|
151
+ validator = @validator_pool.acquire(chunk_type, validator_class,
152
+ chunk, @context)
153
+ begin
154
+ validator.validate
155
+ ensure
156
+ @validator_pool.release(chunk_type, validator)
157
+ end
158
+ @context.mark_chunk_seen(chunk_type, chunk)
159
+ end
160
+ end
161
+
162
+ # Handle unknown chunk types
163
+ #
164
+ # Unknown chunks are checked for safety:
165
+ # - If ancillary (bit 5 of first byte = 1), it's safe to ignore
166
+ # - If critical (bit 5 = 0), it's an error
167
+ #
168
+ # @param chunk [Object] Unknown chunk
169
+ # @return [void]
170
+ def handle_unknown_chunk(chunk)
171
+ chunk_type = chunk.chunk_type.to_s
172
+ first_byte = chunk_type.bytes[0]
173
+
174
+ # Bit 5 (0x20) of first byte indicates ancillary vs critical
175
+ if (first_byte & 0x20).zero?
176
+ # Critical chunk - must be recognized
177
+ @context.add_error(
178
+ chunk_type: chunk_type,
179
+ message: "Unknown critical chunk type: #{chunk_type}",
180
+ severity: :error,
181
+ offset: chunk.abs_offset,
182
+ )
183
+ else
184
+ # Ancillary chunk - safe to ignore
185
+ @context.add_error(
186
+ chunk_type: chunk_type,
187
+ message: "Unknown ancillary chunk type: #{chunk_type} (ignored)",
188
+ severity: :info,
189
+ offset: chunk.abs_offset,
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end