Module: Emf::Emr::Parser

Defined in:
lib/emf/emr/parser.rb

Constant Summary collapse

VARIABLE_ARRAY_RECORDS =

Per-type byte-size validation for records with variable-length arrays. If the declared count exceeds what nSize can hold, the bytes are corrupt and we should preserve them as Raw rather than letting bindata allocate a giant array. The element size is bytes per array entry.

{
  Binary::TypeCodes::POLYGON => { count_offset: 24, element_size: 8 },
  Binary::TypeCodes::POLYLINE => { count_offset: 24, element_size: 8 },
  Binary::TypeCodes::POLYBEZIER => { count_offset: 24, element_size: 8 },
  Binary::TypeCodes::POLYBEZIERTO => { count_offset: 24, element_size: 8 },
  Binary::TypeCodes::POLYLINETO => { count_offset: 24, element_size: 8 },
  Binary::TypeCodes::POLYGON16 => { count_offset: 24, element_size: 4 },
  Binary::TypeCodes::POLYLINE16 => { count_offset: 24, element_size: 4 },
  Binary::TypeCodes::POLYBEZIER16 => { count_offset: 24, element_size: 4 },
  Binary::TypeCodes::POLYBEZIERTO16 => { count_offset: 24, element_size: 4 },
  Binary::TypeCodes::POLYLINETO16 => { count_offset: 24, element_size: 4 },
  Binary::TypeCodes::POLYDRAW => { count_offset: 24, element_size: 9 },
  Binary::TypeCodes::POLYDRAW16 => { count_offset: 24, element_size: 5 },
  # Multi-polygon records have TWO variable arrays: aPolyCounts
  # (nPolys uint32s, 4 bytes each) and aptl/apts (cTotal entries).
  # The count1 entries are ALWAYS uint32 (4 bytes); element_size
  # applies only to the count2 (point) array.
  Binary::TypeCodes::POLYPOLYGON => { count_offset: 24, count2_offset: 28, element_size: 8 },
  Binary::TypeCodes::POLYPOLYLINE => { count_offset: 24, count2_offset: 28, element_size: 8 },
  Binary::TypeCodes::POLYPOLYGON16 => { count_offset: 24, count2_offset: 28, element_size: 4 },
  Binary::TypeCodes::POLYPOLYLINE16 => { count_offset: 24, count2_offset: 28, element_size: 4 }
}.freeze
MAX_SANE_ARRAY_LENGTH =
1_000_000
MAX_INPUT_BYTES =

Bounds the whole-file parse: a 100 MB EMF is unusual; refuse bigger.

200 * 1024 * 1024
EMF_PLUS_IDENTIFIER =

"EMF+" little-endian

0x2B464D45

Class Method Summary collapse

Class Method Details

.build_raw(head, payload) ⇒ Object



165
166
167
# File 'lib/emf/emr/parser.rb', line 165

def build_raw(head, payload)
  Emf::Emr::Binary::Records::Raw.read(head + (payload || +""))
end

.call(bytes) ⇒ Object

Raises:



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/emf/emr/parser.rb', line 10

def call(bytes)
  raise FormatError, "input too large (#{bytes.bytesize} > #{MAX_INPUT_BYTES}); refusing to parse" if bytes.bytesize > MAX_INPUT_BYTES
  raise FormatError, "not an EMF file (signature mismatch)" unless Emf::Detector.call(bytes) == :emf

  io = StringIO.new(bytes, "rb")
  header_wire = read_header(io)
  header = Model::Emr::Header.from_wire(header_wire)

  records = []
  errors = []
  emf_plus_bytes = +""
  trailing = +""

  until io.eof?
    offset = io.pos
    wire = read_record(io, offset, errors)
    break if wire.nil?

    records << Model::Emr::Records::WireAdapter.new(wire: wire, offset: offset)
    emf_plus_bytes << extract_emf_plus(wire) if wire.is_a?(Emf::Emr::Binary::Records::Comment)

    if wire.is_a?(Emf::Emr::Binary::Records::Eof)
      trailing << io.read until io.eof?
      break
    end
  end

  Model::Metafile.new(
    format: emf_plus_bytes.empty? ? :emf : :emf_plus,
    header: header,
    records: records,
    errors: errors,
    emf_plus: emf_plus_bytes.empty? ? nil : emf_plus_bytes,
    trailing: trailing
  )
end

.extract_emf_plus(comment_wire) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/emf/emr/parser.rb', line 171

def extract_emf_plus(comment_wire)
  return +"" unless comment_wire.cb_data&.>= 4

  identifier = comment_wire.data.getbyte(0) |
               (comment_wire.data.getbyte(1) << 8) |
               (comment_wire.data.getbyte(2) << 16) |
               (comment_wire.data.getbyte(3) << 24)
  return +"" unless identifier == EMF_PLUS_IDENTIFIER

  # Payload after the 4-byte identifier is the EMF+ record(s).
  comment_wire.data.bytes[4..].pack("C*")
end

.read_header(io) ⇒ Object

Raises:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/emf/emr/parser.rb', line 47

def read_header(io)
  # Peek at the first 8 bytes to determine the record's nSize.
  head = io.read(8)
  raise FormatError, "truncated EMF header" if head.nil? || head.bytesize < 8

  _type, n_size = head.unpack("VV")
  raise FormatError, "EMF header nSize too small: #{n_size}" if n_size < 8

  io.seek(0)
  full = io.read(n_size)
  if full.nil? || full.bytesize < n_size
    raise FormatError,
          "truncated EMF header (claimed #{n_size}, got #{full&.bytesize || 0})"
  end

  Emf::Emr::Binary::Header.read(full)
end

.read_record(io, offset, errors) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/emf/emr/parser.rb', line 130

def read_record(io, offset, errors)
  head = io.read(8)
  return nil if head.nil? || head.empty?

  type_id, n_size = head.unpack("VV")
  if n_size < 8
    errors << ParseError.new(offset: offset, record_code: type_id,
                             message: "record nSize #{n_size} < 8 bytes (offset #{offset})")
    return nil
  end

  payload = io.read(n_size - 8)
  if payload.nil? || payload.bytesize < n_size - 8
    errors << ParseError.new(offset: offset, record_code: type_id,
                             message: "truncated record at offset #{offset} (claimed #{n_size}, got #{payload&.bytesize || 0})")
    return nil
  end

  unless variable_array_sane?(type_id, n_size, head, payload)
    errors << ParseError.new(
      offset: offset, record_code: type_id,
      message: "variable-array record at offset #{offset} declares " \
               "more entries than nSize allows; preserving as Raw"
    )
    return build_raw(head, payload)
  end

  wire_class = Emf::Emr::Binary::Records.lookup(type_id)
  wire_class.read(head + payload)
rescue StandardError => e
  errors << ParseError.new(offset: offset, record_code: type_id,
                           message: "#{e.class}: #{e.message}")
  build_raw(head, payload)
end

.read_uint32(bytes, offset) ⇒ Object



123
124
125
126
127
128
# File 'lib/emf/emr/parser.rb', line 123

def read_uint32(bytes, offset)
  bytes.getbyte(offset) |
    (bytes.getbyte(offset + 1) << 8) |
    (bytes.getbyte(offset + 2) << 16) |
    (bytes.getbyte(offset + 3) << 24)
end

.variable_array_sane?(type_id, n_size, head, payload) ⇒ Boolean

Returns:

  • (Boolean)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/emf/emr/parser.rb', line 96

def variable_array_sane?(type_id, n_size, head, payload)
  spec = VARIABLE_ARRAY_RECORDS[type_id]
  return true unless spec

  full = head + payload
  return true if full.bytesize < spec[:count_offset] + 4

  count = read_uint32(full, spec[:count_offset])
  return false if count > MAX_SANE_ARRAY_LENGTH

  # For single-array records, element_size is the per-entry size.
  # For multi-array records, the first array (aPolyCounts) entries
  # are always uint32 (4 bytes); element_size applies only to the
  # second array (aptl/apts).
  first_array_entry_size = spec[:count2_offset] ? 4 : spec[:element_size]
  bytes_needed = spec[:count_offset] + 4 + (count * first_array_entry_size)
  return false if bytes_needed > n_size

  # Multi-array records also need to validate the second count.
  return true unless spec[:count2_offset]

  count2 = read_uint32(full, spec[:count2_offset])
  return false if count2 > MAX_SANE_ARRAY_LENGTH

  bytes_needed + (count2 * spec[:element_size]) <= n_size
end