fontisan 0.2.4 → 0.2.5
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 +150 -30
- data/README.adoc +497 -242
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +7 -11
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "basic_validator"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Validators
|
|
7
|
+
# WebFontValidator provides web font optimization and embedding compatibility checks
|
|
8
|
+
#
|
|
9
|
+
# This validator extends BasicValidator with checks specific to web font use cases.
|
|
10
|
+
# Unlike FontBookValidator, it focuses on web embedding permissions, file size,
|
|
11
|
+
# and WOFF/WOFF2 conversion readiness rather than desktop installation.
|
|
12
|
+
#
|
|
13
|
+
# The validator inherits 8 checks from BasicValidator and adds 10 new checks:
|
|
14
|
+
# - Embedding permissions (OS/2 fsType)
|
|
15
|
+
# - File size and glyph complexity for web performance
|
|
16
|
+
# - Character coverage for web use
|
|
17
|
+
# - Glyph accessibility
|
|
18
|
+
# - WOFF/WOFF2 conversion readiness
|
|
19
|
+
#
|
|
20
|
+
# @example Using WebFontValidator
|
|
21
|
+
# validator = WebFontValidator.new
|
|
22
|
+
# report = validator.validate(font)
|
|
23
|
+
# puts "Font is web-ready" if report.valid?
|
|
24
|
+
class WebFontValidator < BasicValidator
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Define web font validation checks
|
|
28
|
+
#
|
|
29
|
+
# Calls super to inherit BasicValidator's 8 checks, then adds 10 new checks.
|
|
30
|
+
# All checks use helpers from Week 1 table implementations.
|
|
31
|
+
def define_checks
|
|
32
|
+
# Inherit BasicValidator checks (8 checks)
|
|
33
|
+
super
|
|
34
|
+
|
|
35
|
+
# Check 9: OS/2 embedding permissions must allow web use
|
|
36
|
+
check_table :embedding_permissions, 'OS/2', severity: :error do |table|
|
|
37
|
+
table.has_embedding_permissions?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Check 10: OS/2 version should be present
|
|
41
|
+
check_table :os2_version_web, 'OS/2', severity: :warning do |table|
|
|
42
|
+
table.valid_version?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check 11: Glyph complexity should be reasonable for web
|
|
46
|
+
check_glyphs :no_complex_glyphs, severity: :warning do |font|
|
|
47
|
+
maxp = font.table('maxp')
|
|
48
|
+
next true unless maxp.version_1_0?
|
|
49
|
+
|
|
50
|
+
# Check max points and contours are reasonable for web rendering
|
|
51
|
+
maxp.max_points && maxp.max_points < 3000 &&
|
|
52
|
+
maxp.max_contours && maxp.max_contours < 500
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check 12: Cmap must have Unicode mapping for web
|
|
56
|
+
check_table :character_coverage, 'cmap', severity: :error do |table|
|
|
57
|
+
table.has_unicode_mapping?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Check 13: Cmap should have BMP coverage
|
|
61
|
+
check_table :cmap_bmp_web, 'cmap', severity: :warning do |table|
|
|
62
|
+
table.has_bmp_coverage?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check 14: Glyf glyphs must be accessible (web browsers need this)
|
|
66
|
+
check_glyphs :glyph_accessible_web, severity: :error do |font|
|
|
67
|
+
glyf = font.table('glyf')
|
|
68
|
+
next true unless glyf
|
|
69
|
+
|
|
70
|
+
loca = font.table('loca')
|
|
71
|
+
head = font.table('head')
|
|
72
|
+
maxp = font.table('maxp')
|
|
73
|
+
glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check 15: Head table must have valid bounding box
|
|
77
|
+
check_table :head_bbox_web, 'head', severity: :error do |table|
|
|
78
|
+
table.valid_bounding_box?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check 16: Hhea metrics must be valid for web rendering
|
|
82
|
+
check_table :hhea_metrics_web, 'hhea', severity: :error do |table|
|
|
83
|
+
table.valid_ascent_descent? && table.valid_number_of_h_metrics?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check 17: WOFF conversion readiness
|
|
87
|
+
check_structure :woff_conversion_ready, severity: :info do |font|
|
|
88
|
+
# Check font can be converted to WOFF
|
|
89
|
+
# All required tables present
|
|
90
|
+
%w[name head maxp hhea].all? { |tag| font.table(tag) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check 18: WOFF2 conversion readiness
|
|
94
|
+
check_structure :woff2_conversion_ready, severity: :info do |font|
|
|
95
|
+
# Check font can be converted to WOFF2
|
|
96
|
+
# Same requirements as WOFF
|
|
97
|
+
%w[name head maxp hhea].all? { |tag| font.table(tag) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/fontisan/version.rb
CHANGED
data/lib/fontisan.rb
CHANGED
|
@@ -124,18 +124,30 @@ require_relative "fontisan/models/color_layer"
|
|
|
124
124
|
require_relative "fontisan/models/color_palette"
|
|
125
125
|
require_relative "fontisan/models/svg_glyph"
|
|
126
126
|
|
|
127
|
+
# Validators infrastructure (NEW - DSL-based framework from Week 2+)
|
|
128
|
+
require_relative "fontisan/validators/validator"
|
|
129
|
+
require_relative "fontisan/validators/basic_validator"
|
|
130
|
+
require_relative "fontisan/validators/font_book_validator"
|
|
131
|
+
require_relative "fontisan/validators/opentype_validator"
|
|
132
|
+
require_relative "fontisan/validators/web_font_validator"
|
|
133
|
+
require_relative "fontisan/validators/profile_loader"
|
|
134
|
+
|
|
127
135
|
# Export infrastructure
|
|
128
136
|
require_relative "fontisan/export/table_serializer"
|
|
129
137
|
require_relative "fontisan/export/ttx_generator"
|
|
130
138
|
require_relative "fontisan/export/ttx_parser"
|
|
131
139
|
require_relative "fontisan/export/exporter"
|
|
132
140
|
|
|
133
|
-
# Validation infrastructure
|
|
134
|
-
|
|
135
|
-
require_relative "fontisan/validation/
|
|
136
|
-
require_relative "fontisan/validation/
|
|
137
|
-
require_relative "fontisan/validation/
|
|
138
|
-
require_relative "fontisan/validation/
|
|
141
|
+
# Validation infrastructure (OLD - commented out for new DSL framework)
|
|
142
|
+
# Week 1 deleted these, Week 2-5 building new DSL-based framework
|
|
143
|
+
# require_relative "fontisan/validation/checks/base_check"
|
|
144
|
+
# require_relative "fontisan/validation/check_registry"
|
|
145
|
+
# require_relative "fontisan/validation/profile"
|
|
146
|
+
# require_relative "fontisan/validation/table_validator"
|
|
147
|
+
# require_relative "fontisan/validation/structure_validator"
|
|
148
|
+
# require_relative "fontisan/validation/consistency_validator"
|
|
149
|
+
# require_relative "fontisan/validation/checksum_validator"
|
|
150
|
+
# require_relative "fontisan/validation/validator"
|
|
139
151
|
|
|
140
152
|
# Subsetting infrastructure
|
|
141
153
|
require_relative "fontisan/subset/options"
|
|
@@ -261,4 +273,64 @@ module Fontisan
|
|
|
261
273
|
def self.info(path, brief: false, font_index: 0)
|
|
262
274
|
Commands::InfoCommand.new(path, brief: brief, font_index: font_index).run
|
|
263
275
|
end
|
|
276
|
+
|
|
277
|
+
# Validate a font file using specified profile
|
|
278
|
+
#
|
|
279
|
+
# Validates fonts against quality checks, structural integrity, and OpenType
|
|
280
|
+
# specification compliance using the new DSL-based validation framework.
|
|
281
|
+
#
|
|
282
|
+
# @param path [String] Path to font file
|
|
283
|
+
# @param profile [Symbol, String] Validation profile (default: :default)
|
|
284
|
+
# Available profiles:
|
|
285
|
+
# - :indexability - Fast validation for font discovery
|
|
286
|
+
# - :usability - Basic usability for installation
|
|
287
|
+
# - :production - Comprehensive quality checks (default)
|
|
288
|
+
# - :web - Web embedding and optimization
|
|
289
|
+
# - :spec_compliance - Full OpenType spec compliance
|
|
290
|
+
# - :default - Alias for production profile
|
|
291
|
+
# @param options [Hash] Additional validation options
|
|
292
|
+
# @return [Models::ValidationReport] Validation report with issues and status
|
|
293
|
+
#
|
|
294
|
+
# @example Validate with default profile
|
|
295
|
+
# report = Fontisan.validate("font.ttf")
|
|
296
|
+
# puts "Valid: #{report.valid?}"
|
|
297
|
+
#
|
|
298
|
+
# @example Validate for web use
|
|
299
|
+
# report = Fontisan.validate("font.ttf", profile: :web)
|
|
300
|
+
# puts "Errors: #{report.summary.errors}"
|
|
301
|
+
#
|
|
302
|
+
# @example Validate and get detailed report
|
|
303
|
+
# report = Fontisan.validate("font.ttf", profile: :production)
|
|
304
|
+
# puts report.to_yaml
|
|
305
|
+
def self.validate(path, profile: :default, options: {})
|
|
306
|
+
# Get profile configuration
|
|
307
|
+
profile_config = Validators::ProfileLoader.profile_info(profile)
|
|
308
|
+
raise ArgumentError, "Unknown profile: #{profile}" unless profile_config
|
|
309
|
+
|
|
310
|
+
# Load font with appropriate mode
|
|
311
|
+
mode = profile_config[:loading_mode].to_sym
|
|
312
|
+
font = FontLoader.load(path, mode: mode)
|
|
313
|
+
|
|
314
|
+
# Load validator for profile
|
|
315
|
+
validator = Validators::ProfileLoader.load(profile)
|
|
316
|
+
|
|
317
|
+
# Run validation
|
|
318
|
+
validator.validate(font)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
class << self
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
# Get loading mode for validation profile
|
|
325
|
+
#
|
|
326
|
+
# Temporarily disabled - will be reimplemented with new DSL framework
|
|
327
|
+
#
|
|
328
|
+
# @param profile [Symbol] Validation profile
|
|
329
|
+
# @return [Symbol] Loading mode (:metadata or :full)
|
|
330
|
+
# def profile_loading_mode(profile)
|
|
331
|
+
# Validation::Profile.load(profile).loading_mode.to_sym
|
|
332
|
+
# rescue
|
|
333
|
+
# :full
|
|
334
|
+
# end
|
|
335
|
+
end
|
|
264
336
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontisan
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -156,7 +156,6 @@ files:
|
|
|
156
156
|
- lib/fontisan/config/scripts.yml
|
|
157
157
|
- lib/fontisan/config/subset_profiles.yml
|
|
158
158
|
- lib/fontisan/config/svg_settings.yml
|
|
159
|
-
- lib/fontisan/config/validation_rules.yml
|
|
160
159
|
- lib/fontisan/config/variable_settings.yml
|
|
161
160
|
- lib/fontisan/config/woff2_settings.yml
|
|
162
161
|
- lib/fontisan/constants.rb
|
|
@@ -322,15 +321,12 @@ files:
|
|
|
322
321
|
- lib/fontisan/utilities/brotli_wrapper.rb
|
|
323
322
|
- lib/fontisan/utilities/checksum_calculator.rb
|
|
324
323
|
- lib/fontisan/utils/thread_pool.rb
|
|
325
|
-
- lib/fontisan/
|
|
326
|
-
- lib/fontisan/
|
|
327
|
-
- lib/fontisan/
|
|
328
|
-
- lib/fontisan/
|
|
329
|
-
- lib/fontisan/
|
|
330
|
-
- lib/fontisan/
|
|
331
|
-
- lib/fontisan/validation/woff2_header_validator.rb
|
|
332
|
-
- lib/fontisan/validation/woff2_table_validator.rb
|
|
333
|
-
- lib/fontisan/validation/woff2_validator.rb
|
|
324
|
+
- lib/fontisan/validators/basic_validator.rb
|
|
325
|
+
- lib/fontisan/validators/font_book_validator.rb
|
|
326
|
+
- lib/fontisan/validators/opentype_validator.rb
|
|
327
|
+
- lib/fontisan/validators/profile_loader.rb
|
|
328
|
+
- lib/fontisan/validators/validator.rb
|
|
329
|
+
- lib/fontisan/validators/web_font_validator.rb
|
|
334
330
|
- lib/fontisan/variable/axis_normalizer.rb
|
|
335
331
|
- lib/fontisan/variable/delta_applicator.rb
|
|
336
332
|
- lib/fontisan/variable/glyph_delta_processor.rb
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# Font Validation Rules Configuration
|
|
2
|
-
#
|
|
3
|
-
# This file defines validation rules for different font types and validation levels.
|
|
4
|
-
# The rules determine which checks are performed and their severity thresholds.
|
|
5
|
-
|
|
6
|
-
# Required tables for different font types
|
|
7
|
-
required_tables:
|
|
8
|
-
# Tables required for all fonts
|
|
9
|
-
all:
|
|
10
|
-
- head
|
|
11
|
-
- name
|
|
12
|
-
- cmap
|
|
13
|
-
- maxp
|
|
14
|
-
- hhea
|
|
15
|
-
- hmtx
|
|
16
|
-
- post
|
|
17
|
-
- OS/2
|
|
18
|
-
|
|
19
|
-
# Additional tables required for TrueType fonts (.ttf)
|
|
20
|
-
truetype:
|
|
21
|
-
- glyf
|
|
22
|
-
- loca
|
|
23
|
-
|
|
24
|
-
# Additional tables required for OpenType/CFF fonts (.otf)
|
|
25
|
-
opentype_cff:
|
|
26
|
-
- "CFF " # or CFF2 for CFF2 format (note: CFF has trailing space per OpenType spec)
|
|
27
|
-
|
|
28
|
-
# Additional tables required for variable fonts
|
|
29
|
-
variable:
|
|
30
|
-
- fvar
|
|
31
|
-
- gvar # For TrueType variable fonts
|
|
32
|
-
- HVAR # Horizontal metrics variations (optional but recommended)
|
|
33
|
-
- MVAR # Metrics variations (optional)
|
|
34
|
-
|
|
35
|
-
# Validation levels define which checks to perform
|
|
36
|
-
validation_levels:
|
|
37
|
-
# Strict: All checks must pass, no warnings allowed
|
|
38
|
-
strict:
|
|
39
|
-
check_required_tables: true
|
|
40
|
-
check_table_checksums: true
|
|
41
|
-
check_head_checksum_adjustment: true
|
|
42
|
-
check_glyph_consistency: true
|
|
43
|
-
check_cmap_references: true
|
|
44
|
-
check_hmtx_consistency: true
|
|
45
|
-
check_name_consistency: true
|
|
46
|
-
check_variable_consistency: true
|
|
47
|
-
check_table_offsets: true
|
|
48
|
-
check_table_ordering: false # Not critical for correctness
|
|
49
|
-
allow_warnings: false
|
|
50
|
-
allow_info: true
|
|
51
|
-
|
|
52
|
-
# Standard: Most checks enabled, warnings allowed
|
|
53
|
-
standard:
|
|
54
|
-
check_required_tables: true
|
|
55
|
-
check_table_checksums: true
|
|
56
|
-
check_head_checksum_adjustment: true
|
|
57
|
-
check_glyph_consistency: true
|
|
58
|
-
check_cmap_references: true
|
|
59
|
-
check_hmtx_consistency: true
|
|
60
|
-
check_name_consistency: true
|
|
61
|
-
check_variable_consistency: true
|
|
62
|
-
check_table_offsets: true
|
|
63
|
-
check_table_ordering: false
|
|
64
|
-
allow_warnings: true
|
|
65
|
-
allow_info: true
|
|
66
|
-
|
|
67
|
-
# Lenient: Basic checks only, many warnings allowed
|
|
68
|
-
lenient:
|
|
69
|
-
check_required_tables: true
|
|
70
|
-
check_table_checksums: false # Skip checksum validation
|
|
71
|
-
check_head_checksum_adjustment: false
|
|
72
|
-
check_glyph_consistency: true
|
|
73
|
-
check_cmap_references: false # Skip cmap validation
|
|
74
|
-
check_hmtx_consistency: true
|
|
75
|
-
check_name_consistency: false
|
|
76
|
-
check_variable_consistency: false
|
|
77
|
-
check_table_offsets: true
|
|
78
|
-
check_table_ordering: false
|
|
79
|
-
allow_warnings: true
|
|
80
|
-
allow_info: true
|
|
81
|
-
|
|
82
|
-
# Error messages templates
|
|
83
|
-
error_messages:
|
|
84
|
-
missing_table: "Missing required table: %{table}"
|
|
85
|
-
invalid_table_checksum: "Table '%{table}' checksum mismatch (expected: %{expected}, got: %{actual})"
|
|
86
|
-
invalid_head_checksum: "Invalid head table checksum adjustment"
|
|
87
|
-
glyph_count_mismatch: "Glyph count mismatch: maxp=%{maxp}, actual=%{actual}"
|
|
88
|
-
invalid_cmap_reference: "cmap references non-existent glyph ID %{glyph_id}"
|
|
89
|
-
hmtx_count_mismatch: "hmtx entries (%{hmtx}) don't match glyph count (%{glyph_count})"
|
|
90
|
-
invalid_table_offset: "Table '%{table}' has invalid offset: %{offset}"
|
|
91
|
-
variable_table_missing: "Variable font missing required table: %{table}"
|
|
92
|
-
fvar_gvar_mismatch: "fvar axis count (%{fvar}) doesn't match gvar axis count (%{gvar})"
|
|
93
|
-
|
|
94
|
-
# Warning messages templates
|
|
95
|
-
warning_messages:
|
|
96
|
-
table_checksum_mismatch: "Table '%{table}' checksum mismatch"
|
|
97
|
-
suboptimal_table_order: "Tables not in optimal order for performance"
|
|
98
|
-
missing_optional_table: "Optional table '%{table}' not present (recommended for %{reason})"
|
|
99
|
-
name_inconsistency: "Name table inconsistency: %{issue}"
|
|
100
|
-
large_font_size: "Font file size (%{size} bytes) is unusually large"
|
|
101
|
-
|
|
102
|
-
# Info messages templates
|
|
103
|
-
info_messages:
|
|
104
|
-
optimization_opportunity: "Font could benefit from %{optimization}"
|
|
105
|
-
subsetting_recommended: "Font contains %{glyph_count} glyphs; subsetting may reduce size"
|
|
106
|
-
compression_recommended: "Font could benefit from WOFF2 compression"
|
|
107
|
-
|
|
108
|
-
# Checksum validation settings
|
|
109
|
-
checksum_validation:
|
|
110
|
-
# Magic number for checksum adjustment calculation
|
|
111
|
-
magic: 0xB1B0AFBA
|
|
112
|
-
|
|
113
|
-
# Tables exempt from checksum validation (some tables have dynamic content)
|
|
114
|
-
skip_tables:
|
|
115
|
-
- DSIG # Digital signature table (changes with signing)
|
|
116
|
-
|
|
117
|
-
# Consistency checks configuration
|
|
118
|
-
consistency_checks:
|
|
119
|
-
# Maximum acceptable glyph ID for cmap validation
|
|
120
|
-
max_glyph_id_multiplier: 2.0 # Allow up to 2x maxp.numGlyphs for safety
|
|
121
|
-
|
|
122
|
-
# Minimum acceptable glyph count
|
|
123
|
-
min_glyph_count: 1
|
|
124
|
-
|
|
125
|
-
# Maximum acceptable glyph count (sanity check)
|
|
126
|
-
max_glyph_count: 65536
|
|
127
|
-
|
|
128
|
-
# Structure validation settings
|
|
129
|
-
structure_validation:
|
|
130
|
-
# Minimum valid offset (after header and directory)
|
|
131
|
-
min_table_offset: 12
|
|
132
|
-
|
|
133
|
-
# Table alignment requirement (4-byte alignment)
|
|
134
|
-
table_alignment: 4
|
|
135
|
-
|
|
136
|
-
# Maximum reasonable table size (100MB)
|
|
137
|
-
max_table_size: 104857600
|
|
138
|
-
|
|
139
|
-
# Variable font specific validation
|
|
140
|
-
variable_validation:
|
|
141
|
-
# Check that all required variation tables are consistent
|
|
142
|
-
check_axis_consistency: true
|
|
143
|
-
|
|
144
|
-
# Check that variation regions are valid
|
|
145
|
-
check_region_validity: true
|
|
146
|
-
|
|
147
|
-
# Check that deltas are within reasonable bounds
|
|
148
|
-
max_delta_value: 32767
|
|
149
|
-
min_delta_value: -32768
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../utilities/checksum_calculator"
|
|
4
|
-
|
|
5
|
-
module Fontisan
|
|
6
|
-
module Validation
|
|
7
|
-
# ChecksumValidator validates font file and table checksums
|
|
8
|
-
#
|
|
9
|
-
# This validator checks that the head table checksum adjustment is correct
|
|
10
|
-
# and validates individual table checksums to ensure file integrity.
|
|
11
|
-
#
|
|
12
|
-
# Single Responsibility: Checksum validation and file integrity
|
|
13
|
-
#
|
|
14
|
-
# @example Validating checksums
|
|
15
|
-
# validator = ChecksumValidator.new(rules)
|
|
16
|
-
# issues = validator.validate(font, font_path)
|
|
17
|
-
class ChecksumValidator
|
|
18
|
-
# Initialize checksum validator
|
|
19
|
-
#
|
|
20
|
-
# @param rules [Hash] Validation rules configuration
|
|
21
|
-
def initialize(rules)
|
|
22
|
-
@rules = rules
|
|
23
|
-
@checksum_config = rules["checksum_validation"] || {}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Validate font checksums
|
|
27
|
-
#
|
|
28
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font to validate
|
|
29
|
-
# @param font_path [String] Path to the font file
|
|
30
|
-
# @return [Array<Hash>] Array of validation issues
|
|
31
|
-
def validate(font, font_path)
|
|
32
|
-
issues = []
|
|
33
|
-
|
|
34
|
-
# Check head table checksum adjustment if enabled
|
|
35
|
-
if should_check?("check_head_checksum_adjustment")
|
|
36
|
-
issues.concat(check_head_checksum_adjustment(font, font_path))
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Check individual table checksums if enabled
|
|
40
|
-
if should_check?("check_table_checksums")
|
|
41
|
-
issues.concat(check_table_checksums(font))
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
issues
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# Check if a validation should be performed
|
|
50
|
-
#
|
|
51
|
-
# @param check_name [String] The check name
|
|
52
|
-
# @return [Boolean] true if check should be performed
|
|
53
|
-
def should_check?(check_name)
|
|
54
|
-
@rules.dig("validation_levels", "standard", check_name)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Check head table checksum adjustment
|
|
58
|
-
#
|
|
59
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
60
|
-
# @param font_path [String] Path to the font file
|
|
61
|
-
# @return [Array<Hash>] Array of checksum issues
|
|
62
|
-
def check_head_checksum_adjustment(font, font_path)
|
|
63
|
-
issues = []
|
|
64
|
-
|
|
65
|
-
head_entry = font.head_table
|
|
66
|
-
return issues unless head_entry
|
|
67
|
-
|
|
68
|
-
# Calculate the checksum of the entire font file
|
|
69
|
-
begin
|
|
70
|
-
file_checksum = Utilities::ChecksumCalculator.calculate_file_checksum(font_path)
|
|
71
|
-
magic = @checksum_config["magic"] || Constants::CHECKSUM_ADJUSTMENT_MAGIC
|
|
72
|
-
|
|
73
|
-
# Read the actual checksum adjustment from head table
|
|
74
|
-
head_data = font.table_data[Constants::HEAD_TAG]
|
|
75
|
-
return issues unless head_data && head_data.bytesize >= 12
|
|
76
|
-
|
|
77
|
-
actual_adjustment = head_data.byteslice(8, 4).unpack1("N")
|
|
78
|
-
|
|
79
|
-
# The actual adjustment should be 0 when we calculate, since we zero it out
|
|
80
|
-
# So we need to check if the file checksum with zeroed adjustment equals magic
|
|
81
|
-
if file_checksum != magic
|
|
82
|
-
# Calculate what the adjustment should be
|
|
83
|
-
temp_checksum = (file_checksum - actual_adjustment) & 0xFFFFFFFF
|
|
84
|
-
correct_adjustment = (magic - temp_checksum) & 0xFFFFFFFF
|
|
85
|
-
|
|
86
|
-
if actual_adjustment != correct_adjustment
|
|
87
|
-
issues << {
|
|
88
|
-
severity: "error",
|
|
89
|
-
category: "checksum",
|
|
90
|
-
message: "Invalid head table checksum adjustment (expected: 0x#{correct_adjustment.to_s(16)}, got: 0x#{actual_adjustment.to_s(16)})",
|
|
91
|
-
location: "head table",
|
|
92
|
-
}
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
rescue StandardError => e
|
|
96
|
-
issues << {
|
|
97
|
-
severity: "error",
|
|
98
|
-
category: "checksum",
|
|
99
|
-
message: "Failed to validate head checksum adjustment: #{e.message}",
|
|
100
|
-
location: "head table",
|
|
101
|
-
}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
issues
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Check individual table checksums
|
|
108
|
-
#
|
|
109
|
-
# @param font [TrueTypeFont, OpenTypeFont] The font
|
|
110
|
-
# @return [Array<Hash>] Array of table checksum issues
|
|
111
|
-
def check_table_checksums(font)
|
|
112
|
-
issues = []
|
|
113
|
-
|
|
114
|
-
skip_tables = @checksum_config["skip_tables"] || []
|
|
115
|
-
|
|
116
|
-
font.tables.each do |table_entry|
|
|
117
|
-
tag = table_entry.tag.to_s # Convert BinData field to string
|
|
118
|
-
|
|
119
|
-
# Skip tables that are exempt from checksum validation
|
|
120
|
-
next if skip_tables.include?(tag)
|
|
121
|
-
|
|
122
|
-
# Get table data
|
|
123
|
-
table_data = font.table_data[tag]
|
|
124
|
-
next unless table_data
|
|
125
|
-
|
|
126
|
-
# Calculate checksum for the table
|
|
127
|
-
calculated_checksum = calculate_table_checksum(table_data)
|
|
128
|
-
declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
|
|
129
|
-
|
|
130
|
-
# Special handling for head table (checksum adjustment field should be 0)
|
|
131
|
-
if tag == Constants::HEAD_TAG
|
|
132
|
-
# Zero out checksum adjustment field for calculation
|
|
133
|
-
modified_data = table_data.dup
|
|
134
|
-
modified_data[8, 4] = "\x00\x00\x00\x00"
|
|
135
|
-
calculated_checksum = calculate_table_checksum(modified_data)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
if calculated_checksum != declared_checksum
|
|
139
|
-
issues << {
|
|
140
|
-
severity: "warning",
|
|
141
|
-
category: "checksum",
|
|
142
|
-
message: "Table '#{tag}' checksum mismatch (expected: 0x#{declared_checksum.to_s(16)}, got: 0x#{calculated_checksum.to_s(16)})",
|
|
143
|
-
location: "#{tag} table",
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
issues
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Calculate checksum for table data
|
|
152
|
-
#
|
|
153
|
-
# @param data [String] The table data
|
|
154
|
-
# @return [Integer] The calculated checksum
|
|
155
|
-
def calculate_table_checksum(data)
|
|
156
|
-
sum = 0
|
|
157
|
-
# Pad to 4-byte boundary
|
|
158
|
-
padded_data = data + ("\x00" * ((4 - (data.bytesize % 4)) % 4))
|
|
159
|
-
|
|
160
|
-
# Sum all 32-bit values
|
|
161
|
-
(0...padded_data.bytesize).step(4) do |i|
|
|
162
|
-
value = padded_data.byteslice(i, 4).unpack1("N")
|
|
163
|
-
sum = (sum + value) & 0xFFFFFFFF
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
sum
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|