Class: Fontisan::Audit::Checks::TableDirectoryCheck

Inherits:
Fontisan::Audit::Check show all
Defined in:
lib/fontisan/audit/checks/table_directory_check.rb

Overview

Validates the SFNT table directory: checksums, 4-byte alignment, offset arithmetic, search-range/entry-selector/range-shift consistency.

These checks are the foundation of all font validation — a corrupt table directory makes every downstream parse unreliable. Replaces the table-directory portion of MS Font Validator + ots-sanitize.

Class Method Summary collapse

Methods inherited from Fontisan::Audit::Check

issue

Class Method Details

.call(font) ⇒ Array<Models::ValidationReport::Issue>

Parameters:

Returns:



19
20
21
22
23
24
25
26
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 19

def self.call(font)
  issues = []
  issues.concat(validate_search_fields(font))
  issues.concat(validate_alignment(font))
  issues.concat(validate_checksums(font))
  issues.concat(validate_offsets(font))
  issues
end

.codeObject



28
29
30
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 28

def self.code
  :table_directory
end

.validate_alignment(font) ⇒ Object

---------- 4-byte alignment ----------



74
75
76
77
78
79
80
81
82
83
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 74

def self.validate_alignment(font)
  font.tables.each_with_object([]) do |entry, issues|
    next if (entry.offset % 4).zero?

    issues << issue(severity: :warning,
                    message: "Table '#{readable_tag(entry.tag)}' at offset " \
                             "#{entry.offset} is not 4-byte aligned",
                    location: "tables.#{readable_tag(entry.tag)}.offset")
  end
end

.validate_checksums(font) ⇒ Object

The 'head' table checksum has a special rule: its checkSumAdjustment field (offset 8) is set to make the whole-table checksum match the directory entry. We skip the head table's own checksum verification and instead verify the adjustment is present.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 91

def self.validate_checksums(font)
  font.tables.each_with_object([]) do |entry, issues|
    tag = readable_tag(entry.tag)
    raw = font.table_data[tag]
    next unless raw

    next if tag == "head" # checkSumAdjustment complicates this

    actual = checksum(raw)
    next if actual == entry.checksum

    issues << issue(severity: :error,
                    message: "Table '#{tag}' checksum mismatch: " \
                             "directory=0x#{entry.checksum.to_s(16).upcase} " \
                             "computed=0x#{actual.to_s(16).upcase}",
                    location: "tables.#{tag}.checksum")
  end
end

.validate_offsets(font) ⇒ Object

---------- offset arithmetic ----------



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 123

def self.validate_offsets(font)
  dir_end = 12 + (font.header.num_tables * 16)
  font.tables.each_with_object([]) do |entry, issues|
    tag = readable_tag(entry.tag)

    if entry.offset < dir_end
      issues << issue(severity: :error,
                      message: "Table '#{tag}' offset #{entry.offset} " \
                               "overlaps the table directory " \
                               "(directory ends at #{dir_end})",
                      location: "tables.#{tag}.offset")
    end

    raw = font.table_data[tag]
    if raw && raw.bytesize != entry.table_length
      issues << issue(severity: :error,
                      message: "Table '#{tag}' length mismatch: " \
                               "directory says #{entry.table_length} " \
                               "but actual data is #{raw.bytesize} bytes",
                      location: "tables.#{tag}.table_length")
    end
  end
end

.validate_search_fields(font) ⇒ Object

---------- search range / entry selector / range shift ----------



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/fontisan/audit/checks/table_directory_check.rb', line 34

def self.validate_search_fields(font)
  num_tables = font.header.num_tables
  expected_range = largest_power_of_two_le(num_tables) * 16
  expected_selector = Math.log2(expected_range / 16).to_i
  expected_shift = (num_tables * 16) - expected_range

  issues = []
  if font.header.search_range != expected_range
    issues << issue(severity: :warning,
                    message: "searchRange is #{font.header.search_range} " \
                             "but should be #{expected_range} " \
                             "(numTables=#{num_tables})",
                    location: "header.searchRange")
  end
  if font.header.entry_selector != expected_selector
    issues << issue(severity: :warning,
                    message: "entrySelector is #{font.header.entry_selector} " \
                             "but should be #{expected_selector}",
                    location: "header.entrySelector")
  end
  if font.header.range_shift != expected_shift
    issues << issue(severity: :warning,
                    message: "rangeShift is #{font.header.range_shift} " \
                             "but should be #{expected_shift}",
                    location: "header.rangeShift")
  end
  issues
end