png_conform 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +197 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +323 -0
- data/Gemfile +13 -0
- data/LICENSE +43 -0
- data/README.adoc +859 -0
- data/Rakefile +10 -0
- data/SECURITY.md +147 -0
- data/docs/ARCHITECTURE.adoc +681 -0
- data/docs/CHUNK_TYPES.adoc +450 -0
- data/docs/CLI_OPTIONS.adoc +913 -0
- data/docs/COMPATIBILITY.adoc +616 -0
- data/examples/README.adoc +398 -0
- data/examples/advanced_usage.rb +304 -0
- data/examples/basic_usage.rb +210 -0
- data/exe/png_conform +6 -0
- data/lib/png_conform/analyzers/comparison_analyzer.rb +230 -0
- data/lib/png_conform/analyzers/metrics_analyzer.rb +176 -0
- data/lib/png_conform/analyzers/optimization_analyzer.rb +190 -0
- data/lib/png_conform/analyzers/resolution_analyzer.rb +274 -0
- data/lib/png_conform/bindata/chunk_structure.rb +153 -0
- data/lib/png_conform/bindata/jng_file.rb +79 -0
- data/lib/png_conform/bindata/mng_file.rb +97 -0
- data/lib/png_conform/bindata/png_file.rb +162 -0
- data/lib/png_conform/cli.rb +116 -0
- data/lib/png_conform/commands/check_command.rb +323 -0
- data/lib/png_conform/commands/list_command.rb +67 -0
- data/lib/png_conform/models/chunk.rb +84 -0
- data/lib/png_conform/models/chunk_info.rb +71 -0
- data/lib/png_conform/models/compression_info.rb +49 -0
- data/lib/png_conform/models/decoded_chunk_data.rb +143 -0
- data/lib/png_conform/models/file_analysis.rb +181 -0
- data/lib/png_conform/models/file_info.rb +91 -0
- data/lib/png_conform/models/image_info.rb +52 -0
- data/lib/png_conform/models/validation_error.rb +89 -0
- data/lib/png_conform/models/validation_result.rb +137 -0
- data/lib/png_conform/readers/full_load_reader.rb +113 -0
- data/lib/png_conform/readers/streaming_reader.rb +180 -0
- data/lib/png_conform/reporters/base_reporter.rb +53 -0
- data/lib/png_conform/reporters/color_reporter.rb +65 -0
- data/lib/png_conform/reporters/json_reporter.rb +18 -0
- data/lib/png_conform/reporters/palette_reporter.rb +48 -0
- data/lib/png_conform/reporters/quiet_reporter.rb +18 -0
- data/lib/png_conform/reporters/reporter_factory.rb +108 -0
- data/lib/png_conform/reporters/summary_reporter.rb +65 -0
- data/lib/png_conform/reporters/text_reporter.rb +66 -0
- data/lib/png_conform/reporters/verbose_reporter.rb +87 -0
- data/lib/png_conform/reporters/very_verbose_reporter.rb +33 -0
- data/lib/png_conform/reporters/visual_elements.rb +66 -0
- data/lib/png_conform/reporters/yaml_reporter.rb +18 -0
- data/lib/png_conform/services/profile_manager.rb +242 -0
- data/lib/png_conform/services/validation_service.rb +457 -0
- data/lib/png_conform/services/zlib_validator.rb +270 -0
- data/lib/png_conform/validators/ancillary/bkgd_validator.rb +140 -0
- data/lib/png_conform/validators/ancillary/chrm_validator.rb +178 -0
- data/lib/png_conform/validators/ancillary/cicp_validator.rb +202 -0
- data/lib/png_conform/validators/ancillary/gama_validator.rb +105 -0
- data/lib/png_conform/validators/ancillary/hist_validator.rb +147 -0
- data/lib/png_conform/validators/ancillary/iccp_validator.rb +243 -0
- data/lib/png_conform/validators/ancillary/itxt_validator.rb +280 -0
- data/lib/png_conform/validators/ancillary/mdcv_validator.rb +201 -0
- data/lib/png_conform/validators/ancillary/offs_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/pcal_validator.rb +289 -0
- data/lib/png_conform/validators/ancillary/phys_validator.rb +107 -0
- data/lib/png_conform/validators/ancillary/sbit_validator.rb +176 -0
- data/lib/png_conform/validators/ancillary/scal_validator.rb +180 -0
- data/lib/png_conform/validators/ancillary/splt_validator.rb +223 -0
- data/lib/png_conform/validators/ancillary/srgb_validator.rb +117 -0
- data/lib/png_conform/validators/ancillary/ster_validator.rb +111 -0
- data/lib/png_conform/validators/ancillary/text_validator.rb +129 -0
- data/lib/png_conform/validators/ancillary/time_validator.rb +132 -0
- data/lib/png_conform/validators/ancillary/trns_validator.rb +154 -0
- data/lib/png_conform/validators/ancillary/ztxt_validator.rb +173 -0
- data/lib/png_conform/validators/apng/actl_validator.rb +81 -0
- data/lib/png_conform/validators/apng/fctl_validator.rb +155 -0
- data/lib/png_conform/validators/apng/fdat_validator.rb +117 -0
- data/lib/png_conform/validators/base_validator.rb +241 -0
- data/lib/png_conform/validators/chunk_registry.rb +219 -0
- data/lib/png_conform/validators/critical/idat_validator.rb +77 -0
- data/lib/png_conform/validators/critical/iend_validator.rb +68 -0
- data/lib/png_conform/validators/critical/ihdr_validator.rb +160 -0
- data/lib/png_conform/validators/critical/plte_validator.rb +120 -0
- data/lib/png_conform/validators/jng/jdat_validator.rb +66 -0
- data/lib/png_conform/validators/jng/jhdr_validator.rb +116 -0
- data/lib/png_conform/validators/jng/jsep_validator.rb +66 -0
- data/lib/png_conform/validators/mng/back_validator.rb +87 -0
- data/lib/png_conform/validators/mng/clip_validator.rb +65 -0
- data/lib/png_conform/validators/mng/clon_validator.rb +45 -0
- data/lib/png_conform/validators/mng/defi_validator.rb +104 -0
- data/lib/png_conform/validators/mng/dhdr_validator.rb +104 -0
- data/lib/png_conform/validators/mng/disc_validator.rb +44 -0
- data/lib/png_conform/validators/mng/endl_validator.rb +65 -0
- data/lib/png_conform/validators/mng/fram_validator.rb +91 -0
- data/lib/png_conform/validators/mng/loop_validator.rb +75 -0
- data/lib/png_conform/validators/mng/mend_validator.rb +31 -0
- data/lib/png_conform/validators/mng/mhdr_validator.rb +69 -0
- data/lib/png_conform/validators/mng/move_validator.rb +61 -0
- data/lib/png_conform/validators/mng/save_validator.rb +39 -0
- data/lib/png_conform/validators/mng/seek_validator.rb +42 -0
- data/lib/png_conform/validators/mng/show_validator.rb +52 -0
- data/lib/png_conform/validators/mng/term_validator.rb +84 -0
- data/lib/png_conform/version.rb +5 -0
- data/lib/png_conform.rb +101 -0
- data/png_conform.gemspec +43 -0
- metadata +201 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Validators
|
|
5
|
+
module Apng
|
|
6
|
+
# Validator for fcTL (Frame Control) chunk
|
|
7
|
+
#
|
|
8
|
+
# The fcTL chunk specifies the parameters of each frame in an animated PNG.
|
|
9
|
+
#
|
|
10
|
+
# Structure (26 bytes):
|
|
11
|
+
# - sequence_number (4 bytes): Sequence number (starts at 0)
|
|
12
|
+
# - width (4 bytes): Frame width in pixels
|
|
13
|
+
# - height (4 bytes): Frame height in pixels
|
|
14
|
+
# - x_offset (4 bytes): X position at which to render frame
|
|
15
|
+
# - y_offset (4 bytes): Y position at which to render frame
|
|
16
|
+
# - delay_num (2 bytes): Frame delay numerator
|
|
17
|
+
# - delay_den (2 bytes): Frame delay denominator
|
|
18
|
+
# - dispose_op (1 byte): Disposal operation (0-2)
|
|
19
|
+
# - blend_op (1 byte): Blend operation (0-1)
|
|
20
|
+
#
|
|
21
|
+
# Constraints:
|
|
22
|
+
# - Must have acTL chunk before any fcTL
|
|
23
|
+
# - Frame dimensions must be <= IHDR dimensions
|
|
24
|
+
# - Frame position + size must fit within IHDR dimensions
|
|
25
|
+
# - Sequence numbers must be consecutive
|
|
26
|
+
# - dispose_op: 0=NONE, 1=BACKGROUND, 2=PREVIOUS
|
|
27
|
+
# - blend_op: 0=SOURCE, 1=OVER
|
|
28
|
+
#
|
|
29
|
+
class FctlValidator < BaseValidator
|
|
30
|
+
CHUNK_TYPE = "fcTL"
|
|
31
|
+
EXPECTED_LENGTH = 26
|
|
32
|
+
|
|
33
|
+
DISPOSE_OP_NONE = 0
|
|
34
|
+
DISPOSE_OP_BACKGROUND = 1
|
|
35
|
+
DISPOSE_OP_PREVIOUS = 2
|
|
36
|
+
|
|
37
|
+
BLEND_OP_SOURCE = 0
|
|
38
|
+
BLEND_OP_OVER = 1
|
|
39
|
+
|
|
40
|
+
def validate
|
|
41
|
+
return unless check_crc
|
|
42
|
+
return unless check_length(EXPECTED_LENGTH)
|
|
43
|
+
|
|
44
|
+
validate_structure
|
|
45
|
+
validate_frame_dimensions
|
|
46
|
+
validate_sequence_number
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_structure
|
|
52
|
+
data = chunk.chunk_data
|
|
53
|
+
|
|
54
|
+
# Extract all fields
|
|
55
|
+
sequence_number = data[0..3].unpack1("N")
|
|
56
|
+
width = data[4..7].unpack1("N")
|
|
57
|
+
height = data[8..11].unpack1("N")
|
|
58
|
+
data[12..15].unpack1("N")
|
|
59
|
+
data[16..19].unpack1("N")
|
|
60
|
+
delay_num = data[20..21].unpack1("n")
|
|
61
|
+
delay_den = data[22..23].unpack1("n")
|
|
62
|
+
dispose_op = data[24].unpack1("C")
|
|
63
|
+
blend_op = data[25].unpack1("C")
|
|
64
|
+
|
|
65
|
+
# Store in context
|
|
66
|
+
context.store(:last_fctl_sequence, sequence_number)
|
|
67
|
+
context.store(:fctl_count, (context.retrieve(:fctl_count) || 0) + 1)
|
|
68
|
+
|
|
69
|
+
# Validate frame dimensions
|
|
70
|
+
if width.zero? || height.zero?
|
|
71
|
+
add_error("fcTL width and height must be > 0")
|
|
72
|
+
return false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Validate dispose_op
|
|
76
|
+
unless check_enum(dispose_op, [DISPOSE_OP_NONE, DISPOSE_OP_BACKGROUND,
|
|
77
|
+
DISPOSE_OP_PREVIOUS], "dispose_op")
|
|
78
|
+
return false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Validate blend_op
|
|
82
|
+
unless check_enum(blend_op, [BLEND_OP_SOURCE, BLEND_OP_OVER],
|
|
83
|
+
"blend_op")
|
|
84
|
+
return false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validate delay
|
|
88
|
+
if delay_den.zero?
|
|
89
|
+
# delay_den of 0 means denominator is 100
|
|
90
|
+
context.store(:frame_delay, delay_num / 100.0)
|
|
91
|
+
else
|
|
92
|
+
context.store(:frame_delay, delay_num.to_f / delay_den)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_frame_dimensions
|
|
99
|
+
ihdr_width = context.retrieve(:ihdr_width)
|
|
100
|
+
ihdr_height = context.retrieve(:ihdr_height)
|
|
101
|
+
|
|
102
|
+
return true unless ihdr_width && ihdr_height
|
|
103
|
+
|
|
104
|
+
data = chunk.chunk_data
|
|
105
|
+
width = data[4..7].unpack1("N")
|
|
106
|
+
height = data[8..11].unpack1("N")
|
|
107
|
+
x_offset = data[12..15].unpack1("N")
|
|
108
|
+
y_offset = data[16..19].unpack1("N")
|
|
109
|
+
|
|
110
|
+
# Frame must fit within IHDR dimensions
|
|
111
|
+
if width > ihdr_width || height > ihdr_height
|
|
112
|
+
add_error("fcTL frame dimensions exceed IHDR dimensions")
|
|
113
|
+
return false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Frame position + size must fit within IHDR
|
|
117
|
+
if x_offset + width > ihdr_width
|
|
118
|
+
add_error("fcTL frame extends beyond IHDR width")
|
|
119
|
+
return false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if y_offset + height > ihdr_height
|
|
123
|
+
add_error("fcTL frame extends beyond IHDR height")
|
|
124
|
+
return false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_sequence_number
|
|
131
|
+
# Check if acTL exists
|
|
132
|
+
unless context.retrieve(:actl_num_frames)
|
|
133
|
+
add_error("fcTL requires acTL chunk")
|
|
134
|
+
return false
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
data = chunk.chunk_data
|
|
138
|
+
sequence_number = data[0..3].unpack1("N")
|
|
139
|
+
expected_sequence = context.retrieve(:expected_apng_sequence) || 0
|
|
140
|
+
|
|
141
|
+
if sequence_number != expected_sequence
|
|
142
|
+
add_error("fcTL sequence number mismatch " \
|
|
143
|
+
"(expected #{expected_sequence}, got #{sequence_number})")
|
|
144
|
+
return false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Update expected sequence for next frame chunk
|
|
148
|
+
context.store(:expected_apng_sequence, expected_sequence + 1)
|
|
149
|
+
|
|
150
|
+
true
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PngConform
|
|
4
|
+
module Validators
|
|
5
|
+
module Apng
|
|
6
|
+
# Validator for fdAT (Frame Data) chunk
|
|
7
|
+
#
|
|
8
|
+
# The fdAT chunk contains the compressed image data for a single frame
|
|
9
|
+
# in an animated PNG. It is similar to IDAT but includes a sequence number.
|
|
10
|
+
#
|
|
11
|
+
# Structure:
|
|
12
|
+
# - sequence_number (4 bytes): Sequence number
|
|
13
|
+
# - frame_data (remaining bytes): Compressed image data
|
|
14
|
+
#
|
|
15
|
+
# Constraints:
|
|
16
|
+
# - Must have fcTL chunk before fdAT
|
|
17
|
+
# - Sequence numbers must be consecutive (continuing from fcTL)
|
|
18
|
+
# - Must have acTL chunk present
|
|
19
|
+
# - Frame data must be valid zlib compressed data
|
|
20
|
+
#
|
|
21
|
+
class FdatValidator < BaseValidator
|
|
22
|
+
CHUNK_TYPE = "fdAT"
|
|
23
|
+
MIN_LENGTH = 5 # At least 4 bytes for sequence + 1 byte data
|
|
24
|
+
|
|
25
|
+
def validate
|
|
26
|
+
return unless check_crc
|
|
27
|
+
return unless validate_min_length
|
|
28
|
+
|
|
29
|
+
validate_structure
|
|
30
|
+
validate_sequence_number
|
|
31
|
+
validate_ordering
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_min_length
|
|
37
|
+
actual = chunk.chunk_data.length
|
|
38
|
+
if actual < MIN_LENGTH
|
|
39
|
+
add_error("fdAT chunk too short (#{actual} bytes, " \
|
|
40
|
+
"minimum #{MIN_LENGTH})")
|
|
41
|
+
return false
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_structure
|
|
47
|
+
data = chunk.chunk_data
|
|
48
|
+
|
|
49
|
+
# Extract sequence number
|
|
50
|
+
sequence_number = data[0..3].unpack1("N")
|
|
51
|
+
|
|
52
|
+
# Store in context
|
|
53
|
+
context.store(:last_fdat_sequence, sequence_number)
|
|
54
|
+
context.store(:fdat_count, (context.retrieve(:fdat_count) || 0) + 1)
|
|
55
|
+
|
|
56
|
+
# Frame data is the rest (starting at byte 4)
|
|
57
|
+
frame_data = data[4..]
|
|
58
|
+
|
|
59
|
+
if frame_data.nil? || frame_data.empty?
|
|
60
|
+
add_error("fdAT has no frame data")
|
|
61
|
+
return false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Store frame data length for statistics
|
|
65
|
+
context.store(:fdat_data_length,
|
|
66
|
+
(context.retrieve(:fdat_data_length) || 0) + frame_data.length)
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_sequence_number
|
|
72
|
+
# Check if acTL exists
|
|
73
|
+
unless context.retrieve(:actl_num_frames)
|
|
74
|
+
add_error("fdAT requires acTL chunk")
|
|
75
|
+
return false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
data = chunk.chunk_data
|
|
79
|
+
sequence_number = data[0..3].unpack1("N")
|
|
80
|
+
expected_sequence = context.retrieve(:expected_apng_sequence)
|
|
81
|
+
|
|
82
|
+
# First fdAT should follow fcTL sequences
|
|
83
|
+
if expected_sequence.nil?
|
|
84
|
+
add_error("fdAT must follow fcTL chunk")
|
|
85
|
+
return false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if sequence_number != expected_sequence
|
|
89
|
+
add_error("fdAT sequence number mismatch " \
|
|
90
|
+
"(expected #{expected_sequence}, got #{sequence_number})")
|
|
91
|
+
return false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Update expected sequence for next chunk
|
|
95
|
+
context.store(:expected_apng_sequence, expected_sequence + 1)
|
|
96
|
+
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_ordering
|
|
101
|
+
# fdAT must have corresponding fcTL
|
|
102
|
+
fctl_count = context.retrieve(:fctl_count) || 0
|
|
103
|
+
context.retrieve(:fdat_count) || 0
|
|
104
|
+
|
|
105
|
+
# Each frame should have fcTL followed by fdAT(s)
|
|
106
|
+
# We can have multiple fdAT chunks per frame, but must have at least one fcTL
|
|
107
|
+
if fctl_count.zero?
|
|
108
|
+
add_error("fdAT chunk requires fcTL chunk")
|
|
109
|
+
return false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module PngConform
|
|
6
|
+
module Validators
|
|
7
|
+
# Base class for all chunk validators
|
|
8
|
+
#
|
|
9
|
+
# Validators follow a consistent pattern:
|
|
10
|
+
# 1. Initialize with chunk and context
|
|
11
|
+
# 2. Validate method returns ValidationResult
|
|
12
|
+
# 3. Protected helper methods for specific checks
|
|
13
|
+
#
|
|
14
|
+
# Validators are MECE - each handles exactly one chunk type
|
|
15
|
+
# and validates all aspects of that chunk type completely.
|
|
16
|
+
class BaseValidator
|
|
17
|
+
attr_reader :chunk, :context
|
|
18
|
+
|
|
19
|
+
# Initialize validator with chunk and validation context
|
|
20
|
+
#
|
|
21
|
+
# @param chunk [BinData::Record] The chunk to validate
|
|
22
|
+
# @param context [ValidationContext] The validation context
|
|
23
|
+
def initialize(chunk, context = nil)
|
|
24
|
+
@chunk = chunk
|
|
25
|
+
@context = context || ValidationContext.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Validate the chunk
|
|
29
|
+
#
|
|
30
|
+
# @return [ValidationResult] The validation result
|
|
31
|
+
def validate
|
|
32
|
+
raise NotImplementedError, "Subclasses must implement #validate"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
# Add an error to the validation result
|
|
38
|
+
#
|
|
39
|
+
# @param message [String] The error message
|
|
40
|
+
# @param severity [Symbol] :error, :warning, or :info
|
|
41
|
+
def add_error(message, severity: :error)
|
|
42
|
+
context.add_error(
|
|
43
|
+
chunk_type: chunk.chunk_type,
|
|
44
|
+
message: message,
|
|
45
|
+
severity: severity,
|
|
46
|
+
offset: chunk.abs_offset,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add a warning to the validation result
|
|
51
|
+
#
|
|
52
|
+
# @param message [String] The warning message
|
|
53
|
+
def add_warning(message)
|
|
54
|
+
add_error(message, severity: :warning)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add an info message to the validation result
|
|
58
|
+
#
|
|
59
|
+
# @param message [String] The info message
|
|
60
|
+
def add_info(message)
|
|
61
|
+
add_error(message, severity: :info)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if chunk data length matches expected length
|
|
65
|
+
#
|
|
66
|
+
# @param expected [Integer] Expected data length
|
|
67
|
+
# @return [Boolean] True if length matches
|
|
68
|
+
def check_length(expected)
|
|
69
|
+
actual = chunk.chunk_data.length
|
|
70
|
+
return true if actual == expected
|
|
71
|
+
|
|
72
|
+
add_error("invalid #{chunk.chunk_type} length (#{actual}, " \
|
|
73
|
+
"should be #{expected})")
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check if value is within valid range
|
|
78
|
+
#
|
|
79
|
+
# @param value [Integer] Value to check
|
|
80
|
+
# @param min [Integer] Minimum valid value
|
|
81
|
+
# @param max [Integer] Maximum valid value
|
|
82
|
+
# @param name [String] Name of the value for error message
|
|
83
|
+
# @return [Boolean] True if value is in range
|
|
84
|
+
def check_range(value, min, max, name)
|
|
85
|
+
return true if value >= min && value <= max
|
|
86
|
+
|
|
87
|
+
add_error("invalid #{name} (#{value}, must be #{min}-#{max})")
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if value is one of valid options
|
|
92
|
+
#
|
|
93
|
+
# @param value [Object] Value to check
|
|
94
|
+
# @param valid [Array] Array of valid values
|
|
95
|
+
# @param name [String] Name of the value for error message
|
|
96
|
+
# @return [Boolean] True if value is valid
|
|
97
|
+
def check_enum(value, valid, name)
|
|
98
|
+
return true if valid.include?(value)
|
|
99
|
+
|
|
100
|
+
add_error("invalid #{name} (#{value}, must be one of " \
|
|
101
|
+
"#{valid.join(', ')})")
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if chunk CRC is valid
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean] True if CRC is valid
|
|
108
|
+
def check_crc
|
|
109
|
+
return true if chunk.crc_valid?
|
|
110
|
+
|
|
111
|
+
add_error("CRC error in #{chunk.chunk_type} chunk")
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validation context maintains state during validation
|
|
117
|
+
class ValidationContext
|
|
118
|
+
attr_reader :errors, :chunks_seen, :file_info
|
|
119
|
+
|
|
120
|
+
def initialize
|
|
121
|
+
@errors = []
|
|
122
|
+
@chunks_seen = {}
|
|
123
|
+
@file_info = {}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add an error/warning/info to the context
|
|
127
|
+
#
|
|
128
|
+
# @param chunk_type [String] The chunk type
|
|
129
|
+
# @param message [String] The error message
|
|
130
|
+
# @param severity [Symbol] :error, :warning, or :info
|
|
131
|
+
# @param offset [Integer] File offset of the error
|
|
132
|
+
def add_error(chunk_type:, message:, severity: :error, offset: nil)
|
|
133
|
+
@errors << {
|
|
134
|
+
chunk_type: chunk_type,
|
|
135
|
+
message: message,
|
|
136
|
+
severity: severity,
|
|
137
|
+
offset: offset,
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Record that a chunk type has been seen
|
|
142
|
+
#
|
|
143
|
+
# @param chunk_type [String] The chunk type
|
|
144
|
+
# @param chunk [BinData::Record] The chunk
|
|
145
|
+
def record_chunk(chunk_type, chunk = nil)
|
|
146
|
+
@chunks_seen[chunk_type] ||= []
|
|
147
|
+
@chunks_seen[chunk_type] << chunk if chunk
|
|
148
|
+
end
|
|
149
|
+
alias mark_chunk_seen record_chunk
|
|
150
|
+
|
|
151
|
+
# Check if a chunk type has been seen
|
|
152
|
+
#
|
|
153
|
+
# @param chunk_type [String] The chunk type
|
|
154
|
+
# @return [Boolean] True if chunk type has been seen
|
|
155
|
+
def seen?(chunk_type)
|
|
156
|
+
@chunks_seen.key?(chunk_type)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get chunks of a specific type
|
|
160
|
+
#
|
|
161
|
+
# @param chunk_type [String] The chunk type
|
|
162
|
+
# @return [Array] Array of chunks of that type
|
|
163
|
+
def chunks_of_type(chunk_type)
|
|
164
|
+
@chunks_seen[chunk_type] || []
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Store file information
|
|
168
|
+
#
|
|
169
|
+
# @param key [Symbol] The key
|
|
170
|
+
# @param value [Object] The value
|
|
171
|
+
def store(key, value)
|
|
172
|
+
@file_info[key] = value
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Retrieve file information
|
|
176
|
+
#
|
|
177
|
+
# @param key [Symbol] The key
|
|
178
|
+
# @return [Object] The value
|
|
179
|
+
def retrieve(key)
|
|
180
|
+
@file_info[key]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check if validation has errors
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] True if there are any errors
|
|
186
|
+
def has_errors?
|
|
187
|
+
@errors.any? { |e| e[:severity] == :error }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Check if validation has warnings
|
|
191
|
+
#
|
|
192
|
+
# @return [Boolean] True if there are any warnings
|
|
193
|
+
def has_warnings?
|
|
194
|
+
@errors.any? { |e| e[:severity] == :warning }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Get all errors
|
|
198
|
+
#
|
|
199
|
+
# @return [Array] Array of error hashes
|
|
200
|
+
def all_errors
|
|
201
|
+
@errors.select { |e| e[:severity] == :error }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Get all warnings
|
|
205
|
+
#
|
|
206
|
+
# @return [Array] Array of warning hashes
|
|
207
|
+
def all_warnings
|
|
208
|
+
@errors.select { |e| e[:severity] == :warning }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get all info messages
|
|
212
|
+
#
|
|
213
|
+
# @return [Array] Array of info hashes
|
|
214
|
+
def all_info
|
|
215
|
+
@errors.select { |e| e[:severity] == :info }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Provide attribute-style access to file_info
|
|
219
|
+
#
|
|
220
|
+
# Allows context.width instead of context.retrieve(:width)
|
|
221
|
+
# and context.width = 100 instead of context.store(:width, 100)
|
|
222
|
+
def method_missing(method, *args)
|
|
223
|
+
method_name = method.to_s
|
|
224
|
+
if method_name.end_with?("=")
|
|
225
|
+
# Setter: context.width = 100
|
|
226
|
+
key = method_name.chomp("=").to_sym
|
|
227
|
+
store(key, args.first)
|
|
228
|
+
elsif args.empty?
|
|
229
|
+
# Getter: context.width
|
|
230
|
+
retrieve(method.to_sym)
|
|
231
|
+
else
|
|
232
|
+
super
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def respond_to_missing?(_method, _include_private = false)
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|