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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "readers/streaming_reader"
|
|
4
|
+
require_relative "services/profile_manager"
|
|
5
|
+
require_relative "services/analysis_manager"
|
|
6
|
+
|
|
7
|
+
module PngConform
|
|
8
|
+
# Dependency Injection Container
|
|
9
|
+
#
|
|
10
|
+
# Centralized dependency management for the application.
|
|
11
|
+
# Provides factory methods for creating readers, validators, reporters,
|
|
12
|
+
# and other services with proper dependency injection.
|
|
13
|
+
#
|
|
14
|
+
# This makes testing easier by allowing mock injection and provides
|
|
15
|
+
# a single source of truth for object creation.
|
|
16
|
+
#
|
|
17
|
+
class Container
|
|
18
|
+
class << self
|
|
19
|
+
# Create a reader for the given type and filepath
|
|
20
|
+
#
|
|
21
|
+
# @param type [Symbol] Reader type (:streaming or :full_load)
|
|
22
|
+
# @param filepath [String] Path to PNG file
|
|
23
|
+
# @param options [Hash] Additional options for the reader
|
|
24
|
+
# @return [Object] Reader instance
|
|
25
|
+
def reader(type, filepath, options = {})
|
|
26
|
+
case type
|
|
27
|
+
when :streaming
|
|
28
|
+
io = File.open(filepath, "rb")
|
|
29
|
+
Readers::StreamingReader.new(io, options)
|
|
30
|
+
when :full_load
|
|
31
|
+
Readers::FullLoadReader.new(filepath, options)
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "Unknown reader type: #{type}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Open a reader with automatic file closing
|
|
38
|
+
#
|
|
39
|
+
# @param type [Symbol] Reader type (:streaming or :full_load)
|
|
40
|
+
# @param filepath [String] Path to PNG file
|
|
41
|
+
# @param options [Hash] Additional options for the reader
|
|
42
|
+
# @yield [reader] Reader instance
|
|
43
|
+
# @return [Object] Result of the block
|
|
44
|
+
def open_reader(type, filepath, _options = {}, &block)
|
|
45
|
+
case type
|
|
46
|
+
when :streaming
|
|
47
|
+
Readers::StreamingReader.open(filepath, &block)
|
|
48
|
+
when :full_load
|
|
49
|
+
Readers::FullLoadReader.open(filepath, &block)
|
|
50
|
+
else
|
|
51
|
+
raise ArgumentError, "Unknown reader type: #{type}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create a validator for a chunk
|
|
56
|
+
#
|
|
57
|
+
# Delegates to ChunkRegistry for validator creation
|
|
58
|
+
#
|
|
59
|
+
# @param chunk [Object] Chunk object
|
|
60
|
+
# @param context [ValidationContext] Validation context
|
|
61
|
+
# @return [Object] Validator instance or nil
|
|
62
|
+
def validator(chunk, context)
|
|
63
|
+
require_relative "validators/chunk_registry"
|
|
64
|
+
Validators::ChunkRegistry.create_validator(chunk, context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Create a reporter based on options
|
|
68
|
+
#
|
|
69
|
+
# Delegates to ReporterFactory for reporter creation
|
|
70
|
+
#
|
|
71
|
+
# @param options [Hash] Reporter options
|
|
72
|
+
# @return [Object] Reporter instance
|
|
73
|
+
def reporter(options)
|
|
74
|
+
require_relative "reporters/reporter_factory"
|
|
75
|
+
Reporters::ReporterFactory.create(**options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get the profile manager singleton
|
|
79
|
+
#
|
|
80
|
+
# @return [ProfileManager] Profile manager instance
|
|
81
|
+
def profile_manager
|
|
82
|
+
@profile_manager ||= Services::ProfileManager.new
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Create an analysis manager
|
|
86
|
+
#
|
|
87
|
+
# @param options [Hash] Analysis options
|
|
88
|
+
# @return [AnalysisManager] Analysis manager instance
|
|
89
|
+
def analysis_manager(options = {})
|
|
90
|
+
Services::AnalysisManager.new(options)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create a validation orchestrator
|
|
94
|
+
#
|
|
95
|
+
# @param reader [Object] File reader
|
|
96
|
+
# @param filepath [String] File path
|
|
97
|
+
# @param options [Hash] Validation options
|
|
98
|
+
# @return [ValidationOrchestrator] Orchestrator instance
|
|
99
|
+
def validation_orchestrator(reader, filepath = nil, options = {})
|
|
100
|
+
require_relative "services/validation_orchestrator"
|
|
101
|
+
Services::ValidationOrchestrator.new(reader, filepath, options)
|
|
102
|
+
end
|
|
103
|
+
alias_method :validation_service, :validation_orchestrator
|
|
104
|
+
|
|
105
|
+
# Reset container state
|
|
106
|
+
#
|
|
107
|
+
# Clears cached instances (useful for testing)
|
|
108
|
+
def reset!
|
|
109
|
+
@profile_manager = nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -139,5 +139,38 @@ module PngConform
|
|
|
139
139
|
year, month, day, hour, minute, second)
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
# iDOT chunk decoded data
|
|
144
|
+
class IdotData < DecodedChunkData
|
|
145
|
+
attribute :display_scale, :integer
|
|
146
|
+
attribute :pixel_format, :integer
|
|
147
|
+
attribute :color_space, :integer
|
|
148
|
+
attribute :backing_scale_factor, :integer
|
|
149
|
+
attribute :flags, :integer
|
|
150
|
+
attribute :reserved1, :integer
|
|
151
|
+
attribute :reserved2, :integer
|
|
152
|
+
|
|
153
|
+
def summary
|
|
154
|
+
parts = []
|
|
155
|
+
parts << "display scale: #{display_scale}" if display_scale
|
|
156
|
+
parts << "pixel format: #{pixel_format}" if pixel_format
|
|
157
|
+
parts << "color space: #{color_space}" if color_space
|
|
158
|
+
parts << "backing scale: #{backing_scale_factor}" if backing_scale_factor
|
|
159
|
+
parts.join(", ")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Format all seven values for detailed display
|
|
163
|
+
def detailed_info
|
|
164
|
+
[
|
|
165
|
+
display_scale,
|
|
166
|
+
pixel_format,
|
|
167
|
+
color_space,
|
|
168
|
+
backing_scale_factor,
|
|
169
|
+
flags,
|
|
170
|
+
reserved1,
|
|
171
|
+
reserved2,
|
|
172
|
+
].join(", ")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
142
175
|
end
|
|
143
176
|
end
|
|
@@ -13,15 +13,26 @@ module PngConform
|
|
|
13
13
|
attribute :compression_ratio, :float
|
|
14
14
|
attribute :crc_errors_count, :integer, default: -> { 0 }
|
|
15
15
|
|
|
16
|
+
# Non-serialized hash map for fast chunk type lookup
|
|
17
|
+
attr_reader :chunks_by_type_map
|
|
18
|
+
|
|
16
19
|
# File types
|
|
17
20
|
FILE_TYPE_PNG = "PNG"
|
|
18
21
|
FILE_TYPE_MNG = "MNG"
|
|
19
22
|
FILE_TYPE_JNG = "JNG"
|
|
20
23
|
FILE_TYPE_UNKNOWN = "UNKNOWN"
|
|
21
24
|
|
|
25
|
+
# Initialize with hash map for fast lookups
|
|
26
|
+
def initialize(*args)
|
|
27
|
+
super(*args)
|
|
28
|
+
@chunks_by_type_map = {}
|
|
29
|
+
rebuild_chunks_map
|
|
30
|
+
end
|
|
31
|
+
|
|
22
32
|
# Add a chunk to the result
|
|
23
33
|
def add_chunk(chunk)
|
|
24
34
|
chunks << chunk
|
|
35
|
+
add_to_chunks_map(chunk)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# Add an error to the result
|
|
@@ -90,14 +101,14 @@ module PngConform
|
|
|
90
101
|
chunks.count
|
|
91
102
|
end
|
|
92
103
|
|
|
93
|
-
# Find chunks by type
|
|
104
|
+
# Find chunks by type (O(1) hash lookup)
|
|
94
105
|
def chunks_by_type(type)
|
|
95
|
-
|
|
106
|
+
@chunks_by_type_map[type] || []
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
# Check if file has specific chunk type
|
|
109
|
+
# Check if file has specific chunk type (O(1) hash lookup)
|
|
99
110
|
def has_chunk?(type)
|
|
100
|
-
|
|
111
|
+
@chunks_by_type_map.key?(type) && !@chunks_by_type_map[type].empty?
|
|
101
112
|
end
|
|
102
113
|
|
|
103
114
|
# Get IHDR chunk (PNG/JNG)
|
|
@@ -132,6 +143,21 @@ module PngConform
|
|
|
132
143
|
end
|
|
133
144
|
parts.join("\n")
|
|
134
145
|
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Add chunk to hash map for fast lookup
|
|
150
|
+
def add_to_chunks_map(chunk)
|
|
151
|
+
chunk_type = chunk.type
|
|
152
|
+
@chunks_by_type_map[chunk_type] ||= []
|
|
153
|
+
@chunks_by_type_map[chunk_type] << chunk
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Rebuild hash map from chunks (for deserialization or external modification)
|
|
157
|
+
def rebuild_chunks_map
|
|
158
|
+
@chunks_by_type_map.clear
|
|
159
|
+
chunks.each { |chunk| add_to_chunks_map(chunk) }
|
|
160
|
+
end
|
|
135
161
|
end
|
|
136
162
|
end
|
|
137
163
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Pipelines
|
|
5
|
+
# Result object for pipeline stages
|
|
6
|
+
#
|
|
7
|
+
# Carries state through the validation pipeline.
|
|
8
|
+
#
|
|
9
|
+
class PipelineResult
|
|
10
|
+
attr_accessor :ready_for_analysis
|
|
11
|
+
attr_reader :context, :chunks
|
|
12
|
+
|
|
13
|
+
# Initialize pipeline result
|
|
14
|
+
#
|
|
15
|
+
# @param context [ValidationContext] Validation context
|
|
16
|
+
# @param chunks [Array] Array of chunks
|
|
17
|
+
def initialize(context:, chunks: [])
|
|
18
|
+
@context = context
|
|
19
|
+
@chunks = chunks
|
|
20
|
+
@ready_for_analysis = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Check if validation has errors
|
|
24
|
+
#
|
|
25
|
+
# @return [Boolean] true if there are errors
|
|
26
|
+
def has_errors?
|
|
27
|
+
@context.has_errors?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Check if validation should terminate early
|
|
31
|
+
#
|
|
32
|
+
# @param fail_fast [Boolean] Whether fail_fast is enabled
|
|
33
|
+
# @return [Boolean] true if should terminate
|
|
34
|
+
def should_terminate?(fail_fast: false)
|
|
35
|
+
fail_fast && has_errors?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_stage"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Pipelines
|
|
7
|
+
module Stages
|
|
8
|
+
# Analysis enrichment stage
|
|
9
|
+
#
|
|
10
|
+
# Runs conditional analyzers based on options.
|
|
11
|
+
#
|
|
12
|
+
class AnalysisStage < BaseStage
|
|
13
|
+
# Initialize analysis stage
|
|
14
|
+
#
|
|
15
|
+
# @param context [ValidationContext] Validation context
|
|
16
|
+
# @param options [Hash] Validation options
|
|
17
|
+
def initialize(context, options)
|
|
18
|
+
@context = context
|
|
19
|
+
@options = options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Execute analysis enrichment
|
|
23
|
+
#
|
|
24
|
+
# @param result [PipelineResult] Current pipeline result
|
|
25
|
+
# @return [PipelineResult] Updated pipeline result
|
|
26
|
+
def execute(result)
|
|
27
|
+
# Analysis is performed later during FileAnalysis building
|
|
28
|
+
# This stage just marks the pipeline as ready for analysis
|
|
29
|
+
result.ready_for_analysis = true
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Pipelines
|
|
5
|
+
module Stages
|
|
6
|
+
# Base class for validation pipeline stages
|
|
7
|
+
#
|
|
8
|
+
# All pipeline stages inherit from this class and implement
|
|
9
|
+
# the execute method. This provides a consistent interface
|
|
10
|
+
# for pipeline execution.
|
|
11
|
+
#
|
|
12
|
+
class BaseStage
|
|
13
|
+
# Execute the stage
|
|
14
|
+
#
|
|
15
|
+
# @param result [PipelineResult] Current pipeline result
|
|
16
|
+
# @return [PipelineResult] Updated pipeline result
|
|
17
|
+
def execute(result)
|
|
18
|
+
raise NotImplementedError, "Subclasses must implement #execute"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_stage"
|
|
4
|
+
require_relative "../../services/chunk_processor"
|
|
5
|
+
require_relative "../../models/pipeline_result"
|
|
6
|
+
|
|
7
|
+
module PngConform
|
|
8
|
+
module Pipelines
|
|
9
|
+
module Stages
|
|
10
|
+
# Chunk validation stage
|
|
11
|
+
#
|
|
12
|
+
# Processes all chunks through validation using ChunkProcessor.
|
|
13
|
+
# Caches CRC calculations during initial read to avoid recomputation.
|
|
14
|
+
#
|
|
15
|
+
class ChunkValidationStage < BaseStage
|
|
16
|
+
# Initialize chunk validation stage
|
|
17
|
+
#
|
|
18
|
+
# @param reader [Object] File reader
|
|
19
|
+
# @param context [ValidationContext] Validation context
|
|
20
|
+
# @param options [Hash] Validation options
|
|
21
|
+
def initialize(reader, context, options = {})
|
|
22
|
+
@reader = reader
|
|
23
|
+
@context = context
|
|
24
|
+
@options = options
|
|
25
|
+
@crc_cache = {} # Cache for CRC calculations
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Execute chunk validation
|
|
29
|
+
#
|
|
30
|
+
# @param result [PipelineResult] Current pipeline result
|
|
31
|
+
# @return [PipelineResult] Updated pipeline result
|
|
32
|
+
def execute(result)
|
|
33
|
+
processor = Services::ChunkProcessor.new(@reader, @context, @options)
|
|
34
|
+
processor.process do |chunk|
|
|
35
|
+
# Cache CRC during initial read to avoid recalculation
|
|
36
|
+
cache_crc(chunk)
|
|
37
|
+
result.chunks << chunk
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Cache CRC calculation for a chunk
|
|
46
|
+
#
|
|
47
|
+
# Performance optimization: calculates CRC once during initial read
|
|
48
|
+
# and stores it for later use in result building.
|
|
49
|
+
#
|
|
50
|
+
# @param chunk [Object] Chunk to cache CRC for
|
|
51
|
+
# @return [void]
|
|
52
|
+
def cache_crc(chunk)
|
|
53
|
+
return unless chunk.respond_to?(:chunk_type) && chunk.respond_to?(:data)
|
|
54
|
+
|
|
55
|
+
key = "#{chunk.chunk_type}_#{chunk.data.hash}"
|
|
56
|
+
@crc_cache[key] ||= calculate_crc(chunk)
|
|
57
|
+
|
|
58
|
+
# Store cached CRC on chunk for later access
|
|
59
|
+
chunk.instance_variable_set(:@cached_crc, @crc_cache[key])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Calculate CRC32 for a chunk
|
|
63
|
+
#
|
|
64
|
+
# @param chunk [Object] BinData chunk
|
|
65
|
+
# @return [Integer] CRC32 value
|
|
66
|
+
def calculate_crc(chunk)
|
|
67
|
+
require "zlib"
|
|
68
|
+
# CRC is calculated over chunk type + chunk data
|
|
69
|
+
Zlib.crc32(chunk.chunk_type.to_s + chunk.data.to_s)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_stage"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Pipelines
|
|
7
|
+
module Stages
|
|
8
|
+
# Chunk sequence validation stage
|
|
9
|
+
#
|
|
10
|
+
# Validates high-level chunk sequence requirements:
|
|
11
|
+
# - IHDR must be first chunk
|
|
12
|
+
# - IEND must be last chunk
|
|
13
|
+
# - At least one IDAT chunk required
|
|
14
|
+
#
|
|
15
|
+
class SequenceValidationStage < BaseStage
|
|
16
|
+
# Initialize sequence validation stage
|
|
17
|
+
#
|
|
18
|
+
# @param context [ValidationContext] Validation context
|
|
19
|
+
def initialize(context)
|
|
20
|
+
@context = context
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Execute sequence validation
|
|
24
|
+
#
|
|
25
|
+
# @param result [PipelineResult] Current pipeline result
|
|
26
|
+
# @return [PipelineResult] Updated pipeline result
|
|
27
|
+
def execute(result)
|
|
28
|
+
validate_ihdr_first
|
|
29
|
+
validate_iend_last
|
|
30
|
+
validate_idat_present
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Check that IHDR is the first chunk
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def validate_ihdr_first
|
|
40
|
+
return if @context.seen?("IHDR")
|
|
41
|
+
|
|
42
|
+
@context.add_error(
|
|
43
|
+
chunk_type: "IHDR",
|
|
44
|
+
message: "Missing IHDR chunk (must be first)",
|
|
45
|
+
severity: :error,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check that IEND is the last chunk
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def validate_iend_last
|
|
53
|
+
return if @context.seen?("IEND")
|
|
54
|
+
|
|
55
|
+
@context.add_error(
|
|
56
|
+
chunk_type: "IEND",
|
|
57
|
+
message: "Missing IEND chunk (must be last)",
|
|
58
|
+
severity: :error,
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check that at least one IDAT chunk exists
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
def validate_idat_present
|
|
66
|
+
return if @context.seen?("IDAT")
|
|
67
|
+
|
|
68
|
+
@context.add_error(
|
|
69
|
+
chunk_type: "IDAT",
|
|
70
|
+
message: "Missing IDAT chunk (at least one required)",
|
|
71
|
+
severity: :error,
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_stage"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Pipelines
|
|
7
|
+
module Stages
|
|
8
|
+
# Signature validation stage
|
|
9
|
+
#
|
|
10
|
+
# Validates that the PNG file starts with the correct signature.
|
|
11
|
+
#
|
|
12
|
+
class SignatureValidationStage < BaseStage
|
|
13
|
+
# Initialize signature validation stage
|
|
14
|
+
#
|
|
15
|
+
# @param reader [Object] File reader
|
|
16
|
+
def initialize(reader)
|
|
17
|
+
@reader = reader
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Execute signature validation
|
|
21
|
+
#
|
|
22
|
+
# @param result [PipelineResult] Current pipeline result
|
|
23
|
+
# @return [PipelineResult] Updated pipeline result
|
|
24
|
+
def execute(result)
|
|
25
|
+
sig = @reader.signature
|
|
26
|
+
expected = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
|
|
27
|
+
|
|
28
|
+
if sig != expected
|
|
29
|
+
result.context.add_error(
|
|
30
|
+
chunk_type: "SIGNATURE",
|
|
31
|
+
message: "Invalid PNG signature",
|
|
32
|
+
severity: :error,
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pipeline_result"
|
|
4
|
+
require_relative "stages/base_stage"
|
|
5
|
+
require_relative "stages/signature_validation_stage"
|
|
6
|
+
require_relative "stages/chunk_validation_stage"
|
|
7
|
+
require_relative "stages/sequence_validation_stage"
|
|
8
|
+
require_relative "stages/analysis_stage"
|
|
9
|
+
require_relative "../services/result_builder"
|
|
10
|
+
require_relative "../services/analysis_manager"
|
|
11
|
+
|
|
12
|
+
module PngConform
|
|
13
|
+
module Pipelines
|
|
14
|
+
# Validation pipeline for PNG files
|
|
15
|
+
#
|
|
16
|
+
# Executes validation in a series of stages:
|
|
17
|
+
# 1. Signature validation
|
|
18
|
+
# 2. Chunk validation
|
|
19
|
+
# 3. Sequence validation
|
|
20
|
+
# 4. Analysis preparation
|
|
21
|
+
#
|
|
22
|
+
class ValidationPipeline
|
|
23
|
+
# Initialize validation pipeline
|
|
24
|
+
#
|
|
25
|
+
# @param reader [Object] File reader
|
|
26
|
+
# @param options [Hash] Validation options
|
|
27
|
+
def initialize(reader, options = {})
|
|
28
|
+
@reader = reader
|
|
29
|
+
@options = options
|
|
30
|
+
@context = Validators::ValidationContext.new
|
|
31
|
+
@chunks = []
|
|
32
|
+
build_stages
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Execute the validation pipeline
|
|
36
|
+
#
|
|
37
|
+
# Runs all stages in order. Stops early if fail_fast is enabled
|
|
38
|
+
# and critical errors are encountered.
|
|
39
|
+
#
|
|
40
|
+
# @return [FileAnalysis] Complete file analysis
|
|
41
|
+
def execute
|
|
42
|
+
result = PipelineResult.new(context: @context, chunks: @chunks)
|
|
43
|
+
|
|
44
|
+
@stages.each do |stage|
|
|
45
|
+
result = stage.execute(result)
|
|
46
|
+
|
|
47
|
+
# Early termination for critical errors
|
|
48
|
+
break if result.should_terminate?(fail_fast: @options[:fail_fast])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
build_file_analysis(result)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Build the pipeline stages
|
|
57
|
+
#
|
|
58
|
+
# @return [void]
|
|
59
|
+
def build_stages
|
|
60
|
+
@stages = [
|
|
61
|
+
Stages::SignatureValidationStage.new(@reader),
|
|
62
|
+
Stages::ChunkValidationStage.new(@reader, @context, @options),
|
|
63
|
+
Stages::SequenceValidationStage.new(@context),
|
|
64
|
+
Stages::AnalysisStage.new(@context, @options),
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build complete FileAnalysis from pipeline result
|
|
69
|
+
#
|
|
70
|
+
# @param result [PipelineResult] Pipeline result
|
|
71
|
+
# @return [FileAnalysis] Complete file analysis
|
|
72
|
+
def build_file_analysis(_result)
|
|
73
|
+
result_builder = Services::ResultBuilder.new(
|
|
74
|
+
@reader,
|
|
75
|
+
@options[:filepath],
|
|
76
|
+
@context,
|
|
77
|
+
@chunks,
|
|
78
|
+
@options,
|
|
79
|
+
)
|
|
80
|
+
file_analysis = result_builder.build
|
|
81
|
+
|
|
82
|
+
# Run analyzers through AnalysisManager
|
|
83
|
+
analysis_manager = Services::AnalysisManager.new(@options)
|
|
84
|
+
analysis_manager.enrich(file_analysis)
|
|
85
|
+
|
|
86
|
+
file_analysis
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -21,12 +21,14 @@ module PngConform
|
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
23
|
class FullLoadReader
|
|
24
|
-
attr_reader :io, :png
|
|
24
|
+
attr_reader :io, :png, :total_bytes_read
|
|
25
25
|
|
|
26
26
|
# Initialize a new full-load reader
|
|
27
27
|
#
|
|
28
28
|
# @param filepath_or_io [String, IO] File path or IO object to read from
|
|
29
|
-
|
|
29
|
+
# @param options [Hash] Options for reading behavior
|
|
30
|
+
# @option options [Boolean] :validate_crc (true) Calculate CRC during reading
|
|
31
|
+
def initialize(filepath_or_io, options = {})
|
|
30
32
|
if filepath_or_io.is_a?(String)
|
|
31
33
|
# File path provided
|
|
32
34
|
@filepath = filepath_or_io
|
|
@@ -38,6 +40,8 @@ module PngConform
|
|
|
38
40
|
@owns_io = false
|
|
39
41
|
end
|
|
40
42
|
@png = nil
|
|
43
|
+
@total_bytes_read = 0
|
|
44
|
+
@validate_crc = options.fetch(:validate_crc, true)
|
|
41
45
|
end
|
|
42
46
|
|
|
43
47
|
# Read the entire PNG file structure
|
|
@@ -48,6 +52,11 @@ module PngConform
|
|
|
48
52
|
|
|
49
53
|
@io.rewind
|
|
50
54
|
@png = BinData::PngFile.read(@io)
|
|
55
|
+
|
|
56
|
+
# Calculate total bytes from chunks
|
|
57
|
+
@total_bytes_read = @png.chunks.sum { |c| 12 + c.data_length }
|
|
58
|
+
|
|
59
|
+
@png
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
# Get PNG signature
|
|
@@ -75,9 +84,9 @@ module PngConform
|
|
|
75
84
|
elsif @io.respond_to?(:stat)
|
|
76
85
|
@io.stat.size
|
|
77
86
|
else
|
|
78
|
-
#
|
|
87
|
+
# Use cached total bytes from chunks
|
|
79
88
|
read unless @png
|
|
80
|
-
8 + @
|
|
89
|
+
8 + @total_bytes_read
|
|
81
90
|
end
|
|
82
91
|
end
|
|
83
92
|
|