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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +82 -42
- data/Gemfile +2 -0
- data/README.adoc +3 -2
- data/benchmarks/README.adoc +570 -0
- data/benchmarks/config/default.yml +35 -0
- data/benchmarks/config/full.yml +32 -0
- data/benchmarks/config/quick.yml +32 -0
- data/benchmarks/direct_validation.rb +18 -0
- data/benchmarks/lib/benchmark_runner.rb +204 -0
- data/benchmarks/lib/metrics_collector.rb +193 -0
- data/benchmarks/lib/png_conform_runner.rb +68 -0
- data/benchmarks/lib/pngcheck_runner.rb +67 -0
- data/benchmarks/lib/report_generator.rb +301 -0
- data/benchmarks/lib/tool_runner.rb +104 -0
- data/benchmarks/profile_loading.rb +12 -0
- data/benchmarks/profile_validation.rb +18 -0
- data/benchmarks/results/.gitkeep +0 -0
- data/benchmarks/run_benchmark.rb +159 -0
- data/config/validation_profiles.yml +105 -0
- data/docs/CHUNK_TYPES.adoc +42 -0
- data/examples/README.md +282 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +41 -7
- data/lib/png_conform/analyzers/metrics_analyzer.rb +6 -9
- data/lib/png_conform/analyzers/optimization_analyzer.rb +30 -24
- data/lib/png_conform/analyzers/resolution_analyzer.rb +31 -32
- data/lib/png_conform/cli.rb +12 -0
- data/lib/png_conform/commands/check_command.rb +118 -52
- data/lib/png_conform/configuration.rb +147 -0
- data/lib/png_conform/container.rb +113 -0
- data/lib/png_conform/models/decoded_chunk_data.rb +33 -0
- data/lib/png_conform/models/validation_result.rb +30 -4
- data/lib/png_conform/pipelines/pipeline_result.rb +39 -0
- data/lib/png_conform/pipelines/stages/analysis_stage.rb +35 -0
- data/lib/png_conform/pipelines/stages/base_stage.rb +23 -0
- data/lib/png_conform/pipelines/stages/chunk_validation_stage.rb +74 -0
- data/lib/png_conform/pipelines/stages/sequence_validation_stage.rb +77 -0
- data/lib/png_conform/pipelines/stages/signature_validation_stage.rb +41 -0
- data/lib/png_conform/pipelines/validation_pipeline.rb +90 -0
- data/lib/png_conform/readers/full_load_reader.rb +13 -4
- data/lib/png_conform/readers/streaming_reader.rb +27 -2
- data/lib/png_conform/reporters/color_reporter.rb +17 -14
- data/lib/png_conform/reporters/reporter_factory.rb +18 -11
- data/lib/png_conform/reporters/visual_elements.rb +22 -16
- data/lib/png_conform/services/analysis_manager.rb +120 -0
- data/lib/png_conform/services/chunk_processor.rb +195 -0
- data/lib/png_conform/services/file_signature.rb +226 -0
- data/lib/png_conform/services/file_strategy.rb +78 -0
- data/lib/png_conform/services/lru_cache.rb +170 -0
- data/lib/png_conform/services/parallel_validator.rb +118 -0
- data/lib/png_conform/services/profile_manager.rb +41 -12
- data/lib/png_conform/services/result_builder.rb +299 -0
- data/lib/png_conform/services/validation_cache.rb +210 -0
- data/lib/png_conform/services/validation_orchestrator.rb +188 -0
- data/lib/png_conform/services/validation_service.rb +82 -321
- data/lib/png_conform/services/validator_pool.rb +142 -0
- data/lib/png_conform/utils/colorizer.rb +149 -0
- data/lib/png_conform/validators/ancillary/idot_validator.rb +102 -0
- data/lib/png_conform/validators/chunk_registry.rb +143 -128
- data/lib/png_conform/validators/streaming_idat_validator.rb +123 -0
- data/lib/png_conform/version.rb +1 -1
- data/lib/png_conform.rb +7 -46
- data/png_conform.gemspec +1 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|