dbf 5.3.0 → 5.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2d1ca393ae41262fd052c74ffe0ec497a963a2c036797686061312a0c492888
4
- data.tar.gz: 7e92c62ace7713b883b6b2e925c34f7a8114ebd6345b36a81f97fb47109711e2
3
+ metadata.gz: f296a6fc75892231410af21b2ee94490666f740f425f671078b5c28ded1e2977
4
+ data.tar.gz: eed4dfd84582cdf9c8e125ca2ddb1e882ebe75147321469db26166f7777a1ea1
5
5
  SHA512:
6
- metadata.gz: f0f3b1b02ab53f505ab3efc1dff89d2ad1169698ef7d9443b3eec3b758b921ae2df51bf443d4e877ac072bdef87c547607d1d39d913a5c361b4c31e21fee4b07
7
- data.tar.gz: 6d826dd35aa6e80b671aeb804dc9991776851bbb9ca0ff5fb9ed6cd7139cbd40b0bad68327fc8f17b03b9ba2f2b77479331b7ad998c53e7ee32e5caee59241e6
6
+ metadata.gz: 578f0f6cc7c88a205c38fd2ef93af2d8e5c924483018af4b96fb349dd12f40386c13547dfc67cd6fbdd47e191a0d04ef46bf0e1c38f9baec6057197661cd6945
7
+ data.tar.gz: 6960aba5a5b253cc6041c0822a504714ab5a511fee4ea1ff84f43cde10c8cf7db4d74d46f84e670d49f3a4d7110525a3196ea2f76cbcc37174a657e56a755fae
data/CHANGELOG.md CHANGED
@@ -1,7 +1,26 @@
1
1
  # Changelog
2
2
 
3
- ## main branch
4
-
3
+ ## 5.4.0
4
+
5
+ - CLI: replace terminal control bytes in file-derived output so a crafted DBF cannot emit escape sequences to an interactive terminal; CSV and schema output are only filtered when writing to a terminal, so redirected exports are unchanged
6
+ - Security (CWE-248): write binary or invalidly encoded cells in `Table#to_csv` as representable text instead of raising an encoding error
7
+ - Security (CWE-248): replace unrepresentable column type bytes so a corrupt descriptor cannot raise when the schema is serialized to JSON
8
+ - Security (CWE-248): `Table#record` returns nil when the file ends after the delete flag instead of crashing on a nil record body
9
+ - Security (CWE-248): stop column parsing when the file ends mid-descriptor instead of raising `ArgumentError` from a short unpack
10
+ - Security (CWE-248): guard FoxPro memo-pointer decoding against a nil value from a truncated record instead of crashing on nil.unpack1
11
+ - Security (CWE-248): guard dBase IV memo reads against a nil/short block header instead of crashing on nil.unpack1
12
+ - Security (CWE-248): guard dBase III memo reads against a start block past EOF instead of crashing on a nil block
13
+ - Security (CWE-248): stop column parsing on a truncated descriptor instead of constructing an invalid column that crashes on a nil length
14
+ - Security (CWE-248): decode truncated numeric cells (Currency, AutoIncrement) to blank instead of raising an uncaught `nil` crash
15
+ - Security (CWE-248): handle truncated headers and missing column terminators gracefully instead of raising an uncaught `nil` crash during column parsing
16
+ - Security (CWE-400): bound FoxPro memo reads by the memo file size so a crafted memo size cannot force a ~4 GiB allocation
17
+ - Security (CWE-400): bound dBase IV memo reads by the memo file size so a crafted length field cannot force a ~4 GiB allocation
18
+ - Security (CWE-835): bound record iteration by the bytes actually read so a crafted `record_count` (or zero `record_length`) cannot cause an unbounded loop
19
+ - Security (CWE-789): bound the record read buffer by the file's actual size so a crafted header cannot force a multi-gigabyte allocation from a tiny file
20
+ - Security (CWE-400): resolve FoxPro `.dbc` tables by scanning the directory instead of `Dir.glob`, preventing glob brace-expansion CPU exhaustion from a crafted object name
21
+ - Security (CWE-22): confine Visual FoxPro `.dbc` table resolution to the database directory, preventing path traversal via a crafted container object name
22
+ - Security (CWE-1236): neutralize spreadsheet formula injection in `Table#to_csv` by prefixing a quote to string cells/headers starting with `= + - @`
23
+ - Security (CWE-94): escape table and column names when generating ActiveRecord/Sequel schemas, preventing Ruby code injection from crafted DBF header names
5
24
  - Add support for 8 more code pages (issue #98): Mazovia cp620 and Kamenický cp895 via vendored translation tables (new DBF::Encoder), plus macRoman, cp1255, cp1256, macCyrillic, macCentEuro and macGreek
6
25
  - Blank Visual FoxPro "T" (DateTime) columns now return nil instead of a Julian day-0 date
7
26
  - Blank "F" (Float) columns now return nil instead of 0.0, matching "N" (Number) behavior
data/lib/dbf/cli.rb CHANGED
@@ -14,6 +14,50 @@ module DBF
14
14
  -c = export as CSV
15
15
  HELP
16
16
 
17
+ # Bytes a terminal interprets as control or escape sequences. A crafted
18
+ # DBF can carry these in column names and record values, so they are
19
+ # replaced before file-derived text reaches an interactive terminal.
20
+ CONTROL_BYTES = /[\x00-\x1F\x7F]/n
21
+ # The same, but keeping CR and LF so CSV row separators survive.
22
+ CONTROL_BYTES_KEEPING_NEWLINES = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/n
23
+
24
+ # Replaces terminal control bytes. Substitution happens on a byte copy so
25
+ # a value whose bytes are invalid in its encoding cannot raise here.
26
+ #
27
+ # @param value [Object]
28
+ # @param pattern [Regexp]
29
+ # @return [String]
30
+ def self.sanitize(value, pattern = CONTROL_BYTES)
31
+ string = value.to_s
32
+ string.b.gsub(pattern, '?').force_encoding(string.encoding)
33
+ end
34
+
35
+ # Wraps an IO so text written to an interactive terminal is stripped of
36
+ # control bytes. Redirected or piped output is never wrapped, so exported
37
+ # data is passed through unaltered.
38
+ class TerminalFilter
39
+ def initialize(io)
40
+ @io = io
41
+ end
42
+
43
+ def <<(data)
44
+ @io << CLI.sanitize(data, CONTROL_BYTES_KEEPING_NEWLINES)
45
+ self
46
+ end
47
+
48
+ def write(*data)
49
+ @io.write(*data.map { |datum| CLI.sanitize(datum, CONTROL_BYTES_KEEPING_NEWLINES) })
50
+ end
51
+
52
+ def method_missing(name, ...) # :nodoc:
53
+ @io.respond_to?(name) ? @io.send(name, ...) : super
54
+ end
55
+
56
+ def respond_to_missing?(name, include_private = false) # :nodoc:
57
+ @io.respond_to?(name, include_private) || super
58
+ end
59
+ end
60
+
17
61
  def self.run(argv, stdout: $stdout, stderr: $stderr)
18
62
  new(argv, stdout: stdout, stderr: stderr).run
19
63
  end
@@ -65,11 +109,11 @@ module DBF
65
109
  end
66
110
 
67
111
  def print_ar_schema(filename)
68
- @stdout.puts DBF::Table.new(filename).schema(:activerecord)
112
+ @stdout.puts terminal_safe(DBF::Table.new(filename).schema(:activerecord))
69
113
  end
70
114
 
71
115
  def print_sequel_schema(filename)
72
- @stdout.puts DBF::Table.new(filename).schema(:sequel)
116
+ @stdout.puts terminal_safe(DBF::Table.new(filename).schema(:sequel))
73
117
  end
74
118
 
75
119
  def print_summary(filename)
@@ -84,12 +128,27 @@ module DBF
84
128
  @stdout.puts 'Name Type Length Decimal'
85
129
  @stdout.puts '-' * 78
86
130
  table.columns.each do |f|
87
- @stdout.puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal)
131
+ # Column names and types come from the file. Always replace control
132
+ # bytes here: they are never valid in a name and would otherwise both
133
+ # emit escape sequences and break the column alignment below.
134
+ @stdout.puts format('%-16s %-10s %-10s %-10s', self.class.sanitize(f.name), self.class.sanitize(f.type), f.length, f.decimal)
88
135
  end
89
136
  end
90
137
 
91
138
  def print_csv(filename)
92
- DBF::Table.new(filename).to_csv(@stdout)
139
+ DBF::Table.new(filename).to_csv(interactive? ? TerminalFilter.new(@stdout) : @stdout)
140
+ end
141
+
142
+ # Exported data is only filtered when it is going to a terminal, so
143
+ # redirecting or piping still produces byte-for-byte the original values.
144
+ def terminal_safe(text)
145
+ return text unless interactive?
146
+
147
+ self.class.sanitize(text, CONTROL_BYTES_KEEPING_NEWLINES)
148
+ end
149
+
150
+ def interactive?
151
+ @stdout.respond_to?(:tty?) && @stdout.tty?
93
152
  end
94
153
  end
95
154
  end
data/lib/dbf/column.rb CHANGED
@@ -38,7 +38,7 @@ module DBF
38
38
  def initialize(table, name, type, length, decimal)
39
39
  @table = table
40
40
  @name = clean(name)
41
- @type = type
41
+ @type = clean_type(type)
42
42
  @length = length
43
43
  @decimal = decimal
44
44
 
@@ -85,6 +85,14 @@ module DBF
85
85
  @table.encode_string(value.strip.split("\x00", 2).first || +'')
86
86
  end
87
87
 
88
+ # The column type is a single ASCII character. A corrupt file can supply
89
+ # any byte, which would otherwise stay binary and raise when serialized
90
+ # (e.g. to JSON), so replace anything unrepresentable.
91
+ def clean_type(value) # :nodoc:
92
+ type = value.to_s.dup.force_encoding(Encoding::UTF_8)
93
+ type.valid_encoding? ? type : type.scrub('?')
94
+ end
95
+
88
96
  def type_cast_class # :nodoc:
89
97
  @type_cast_class ||= begin
90
98
  klass = @length == 0 ? ColumnType::Nil : TYPE_CAST_CLASS[type.to_sym]
@@ -12,7 +12,15 @@ module DBF
12
12
  safe_seek do
13
13
  @data.seek(@version_config.header_size)
14
14
  [].tap do |columns|
15
- columns << Column.new(*@version_config.read_column_args(@table, @data)) until end_of_record?
15
+ until end_of_record?
16
+ args = @version_config.read_column_args(@table, @data)
17
+ # A descriptor truncated by EOF is returned as nil, or unpacks to
18
+ # a nil length; stop rather than constructing an invalid column
19
+ # (which would crash on nil < 0).
20
+ break if args.nil? || args[3].nil?
21
+
22
+ columns << Column.new(*args)
23
+ end
16
24
  end
17
25
  end
18
26
  end
@@ -20,7 +28,12 @@ module DBF
20
28
  private
21
29
 
22
30
  def end_of_record?
23
- safe_seek { @data.read(1).ord == 13 }
31
+ safe_seek do
32
+ byte = @data.read(1)
33
+ # A truncated file that ends before the 0x0D column terminator marks
34
+ # the end of the column list rather than crashing on nil.ord.
35
+ byte.nil? || byte.ord == 13
36
+ end
24
37
  end
25
38
 
26
39
  def safe_seek
@@ -20,6 +20,10 @@ module DBF
20
20
  end
21
21
 
22
22
  def decode(raw, &)
23
+ # A record truncated before this column yields a nil slice; treat it
24
+ # as blank rather than crashing in the type cast.
25
+ return blank_value if raw.nil?
26
+
23
27
  if skip_blank? && raw.count(' ') == raw.length
24
28
  blank_value
25
29
  else
@@ -49,7 +53,8 @@ module DBF
49
53
  class Currency < Base
50
54
  # @param value [String]
51
55
  def type_cast(value)
52
- (value.unpack1('q<') / 10_000.0).to_f
56
+ int = value.unpack1('q<')
57
+ int && (int / 10_000.0).to_f
53
58
  end
54
59
  end
55
60
 
@@ -64,6 +69,8 @@ module DBF
64
69
  # @param value [String]
65
70
  def type_cast(value)
66
71
  bits = value.unpack1('B*')
72
+ return nil unless bits && bits.length >= 32
73
+
67
74
  sign_multiplier = bits[0] == '0' ? -1 : 1
68
75
  bits[1, 31].to_i(2) * sign_multiplier
69
76
  end
@@ -47,11 +47,25 @@ module DBF
47
47
  # @param name [String]
48
48
  # @return [String]
49
49
  def table_path(name)
50
- glob = File.join(@dirname, "#{name}.dbf")
51
- path = Dir.glob(glob, File::FNM_CASEFOLD).first
50
+ name = name.to_s
51
+ raise DBF::FileNotFoundError, "related table not found: #{name}" if name.empty? || name.include?("\x00")
52
+
53
+ # Treat the container-supplied name as an untrusted basename so that
54
+ # path separators and ".." cannot escape the database directory.
55
+ # Match case-insensitively by scanning the directory rather than
56
+ # globbing, so glob metacharacters (`{}`, `*`, `?`, `[]`) in the name
57
+ # cannot trigger brace-expansion CPU exhaustion.
58
+ target = "#{File.basename(name)}.dbf"
59
+ entry = Dir.children(@dirname).find { |child| child.casecmp?(target) }
60
+ path = entry && File.join(@dirname, entry)
52
61
 
53
62
  raise DBF::FileNotFoundError, "related table not found: #{name}" unless path && File.exist?(path)
54
63
 
64
+ # Defense in depth: confirm the resolved file really is inside the
65
+ # database directory before opening it.
66
+ contained = File.realpath(path).start_with?("#{File.realpath(@dirname)}#{File::SEPARATOR}")
67
+ raise DBF::FileNotFoundError, "related table not found: #{name}" unless contained
68
+
55
69
  path
56
70
  end
57
71
 
data/lib/dbf/header.rb CHANGED
@@ -2,9 +2,14 @@
2
2
 
3
3
  module DBF
4
4
  class Header
5
+ HEADER_SIZE = 32
6
+
5
7
  attr_reader :version, :record_count, :header_length, :record_length, :encoding_key, :encoding
6
8
 
7
9
  def initialize(data)
10
+ # Pad a nil or truncated header read so unpacking a short file yields
11
+ # empty values instead of raising.
12
+ data = data.to_s.b.ljust(HEADER_SIZE, "\x00")
8
13
  @version = data.unpack1('H2')
9
14
  @encoding_key = nil
10
15
  @encoding = nil
@@ -7,7 +7,12 @@ module DBF
7
7
  data.seek offset(start_block)
8
8
  memo_string = +''
9
9
  loop do
10
- block = data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
10
+ block = data.read(BLOCK_SIZE)
11
+ # A start block past EOF yields nil; return what we have rather
12
+ # than crashing on nil.gsub.
13
+ break if block.nil?
14
+
15
+ block = block.gsub(/(\000|\032)/, '')
11
16
  memo_string << block
12
17
  break if block.size < BLOCK_SIZE
13
18
  end
@@ -5,7 +5,21 @@ module DBF
5
5
  class Dbase4 < Base
6
6
  def build_memo(start_block) # :nodoc:
7
7
  data.seek offset(start_block)
8
- data.read(data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
8
+
9
+ # A start block past EOF yields a nil/short header; return nil rather
10
+ # than crashing on nil.unpack1.
11
+ header = data.read(BLOCK_HEADER_SIZE)
12
+ return nil unless header && header.bytesize == BLOCK_HEADER_SIZE
13
+
14
+ length = header.unpack1('x4L')
15
+
16
+ # Bound the read by the bytes remaining so a crafted 32-bit length
17
+ # field cannot force a ~4 GiB allocation from a small memo file.
18
+ remaining = data.size - data.pos
19
+ length = remaining if length > remaining
20
+ return nil if length <= 0
21
+
22
+ data.read(length)
9
23
  end
10
24
  end
11
25
  end
@@ -23,11 +23,15 @@ module DBF
23
23
  private
24
24
 
25
25
  def read_memo_content(memo_string, memo_size) # :nodoc:
26
- if memo_size > block_content_size
27
- memo_string << @data.read(content_size(memo_size))
28
- else
29
- memo_string[0, memo_size]
30
- end
26
+ return memo_string[0, memo_size] unless memo_size > block_content_size
27
+
28
+ # Bound the read by the bytes remaining so a crafted 32-bit memo_size
29
+ # cannot force a ~4 GiB allocation from a small memo file.
30
+ length = content_size(memo_size)
31
+ remaining = @data.size - @data.pos
32
+ length = remaining if length > remaining
33
+ memo_string << @data.read(length) if length.positive?
34
+ memo_string
31
35
  end
32
36
 
33
37
  def block_size # :nodoc:
data/lib/dbf/record.rb CHANGED
@@ -83,7 +83,9 @@ module DBF
83
83
 
84
84
  def decode_memo_value(raw) # :nodoc:
85
85
  memo = @context.memo
86
- return nil unless memo
86
+ # A record truncated before the memo column yields a nil pointer; skip
87
+ # decoding rather than crashing on nil.unpack1.
88
+ return nil unless memo && raw
87
89
 
88
90
  version = @context.version
89
91
  raw = raw.unpack1('V') if version == '30' || version == '31'
@@ -14,8 +14,13 @@ module DBF
14
14
  buf = read_buffer
15
15
  return unless buf
16
16
 
17
+ # Bound the iteration by the bytes actually read so a crafted
18
+ # record_count (or record_length == 0) cannot drive an unbounded loop.
19
+ max_records = @record_length > 0 ? buf.bytesize / @record_length : 0
20
+ count = @record_count < max_records ? @record_count : max_records
21
+
17
22
  pos = 0
18
- @record_count.times do
23
+ count.times do
19
24
  if buf.getbyte(pos) == 0x2A
20
25
  yield nil
21
26
  else
@@ -29,7 +34,14 @@ module DBF
29
34
 
30
35
  def read_buffer
31
36
  @data.seek(@header_length)
32
- @data.read(@record_length * @record_count)
37
+
38
+ # Bound the allocation by the bytes actually available so a crafted
39
+ # header (huge record_length * record_count) cannot force a giant read
40
+ # from a tiny file.
41
+ requested = @record_length * @record_count
42
+ available = @data.size - @header_length
43
+ available = 0 if available.negative?
44
+ @data.read(requested < available ? requested : available)
33
45
  end
34
46
  end
35
47
  end
data/lib/dbf/schema.rb CHANGED
@@ -55,7 +55,7 @@ module DBF
55
55
 
56
56
  def activerecord_schema(*) # :nodoc:
57
57
  output = +"ActiveRecord::Schema.define do\n"
58
- output << " create_table \"#{name}\" do |t|\n"
58
+ output << " create_table #{name.to_s.inspect} do |t|\n"
59
59
  columns.each do |column|
60
60
  output << " t.column #{activerecord_schema_definition(column)}"
61
61
  end
@@ -66,7 +66,7 @@ module DBF
66
66
  def sequel_schema(table_only: false) # :nodoc:
67
67
  output = +''
68
68
  output << "Sequel.migration do\n change do\n " unless table_only
69
- output << " create_table(:#{name}) do\n"
69
+ output << " create_table(#{name.to_s.to_sym.inspect}) do\n"
70
70
  columns.each do |column|
71
71
  output << " column #{sequel_schema_definition(column)}"
72
72
  end
@@ -84,7 +84,7 @@ module DBF
84
84
  # @param column [DBF::Column]
85
85
  # @return [String]
86
86
  def activerecord_schema_definition(column)
87
- "\"#{column.underscored_name}\", #{schema_data_type(column, :activerecord)}\n"
87
+ "#{column.underscored_name.inspect}, #{schema_data_type(column, :activerecord)}\n"
88
88
  end
89
89
 
90
90
  # Sequel schema definition
@@ -92,7 +92,7 @@ module DBF
92
92
  # @param column [DBF::Column]
93
93
  # @return [String]
94
94
  def sequel_schema_definition(column)
95
- ":#{column.underscored_name}, #{schema_data_type(column, :sequel)}\n"
95
+ "#{column.underscored_name.to_sym.inspect}, #{schema_data_type(column, :sequel)}\n"
96
96
  end
97
97
 
98
98
  def schema_data_type(column, format = :activerecord) # :nodoc:
data/lib/dbf/table.rb CHANGED
@@ -7,6 +7,10 @@ module DBF
7
7
  class NoColumnsDefined < StandardError
8
8
  end
9
9
 
10
+ # Leading bytes that make a spreadsheet treat a CSV cell as a formula:
11
+ # "=", "+", "-", "@", tab, and carriage return.
12
+ CSV_FORMULA_TRIGGERS = [0x3D, 0x2B, 0x2D, 0x40, 0x09, 0x0D].freeze
13
+
10
14
  # DBF::Table is the primary interface to a single DBF file and provides
11
15
  # methods for enumerating and searching the records.
12
16
  class Table
@@ -132,6 +136,10 @@ module DBF
132
136
  return nil if deleted_record?
133
137
 
134
138
  record_data = @data.read(record_length)
139
+ # A file that ends immediately after the delete flag has no record body;
140
+ # treat it as absent rather than building a Record over nil data.
141
+ return nil unless record_data
142
+
135
143
  DBF::Record.new(record_data, record_context)
136
144
  end
137
145
 
@@ -148,8 +156,8 @@ module DBF
148
156
  else path_or_io
149
157
  end
150
158
  csv = CSV.new(io, force_quotes: true)
151
- csv << column_names
152
- each { |record| csv << record.to_a }
159
+ csv << column_names.map { |name| csv_safe_value(name) }
160
+ each { |record| csv << record.to_a.map { |value| csv_safe_value(value) } }
153
161
  end
154
162
 
155
163
  # Human readable version description
@@ -176,6 +184,31 @@ module DBF
176
184
 
177
185
  private
178
186
 
187
+ # Neutralizes spreadsheet formula injection (CWE-1236) on CSV export by
188
+ # prefixing a single quote to string cells that begin with a formula
189
+ # trigger character. Non-string values (numbers, dates, booleans) are
190
+ # returned unchanged. The leading byte is compared numerically so that a
191
+ # value whose bytes are invalid in its encoding cannot raise here.
192
+ def csv_safe_value(value) # :nodoc:
193
+ return value unless value.is_a?(::String)
194
+
195
+ value = csv_compatible(value)
196
+ return value unless CSV_FORMULA_TRIGGERS.include?(value.getbyte(0))
197
+
198
+ quote = +"'"
199
+ quote.force_encoding(value.encoding) + value
200
+ end
201
+
202
+ # A row is written as a single string, so a binary (General/OLE) or
203
+ # invalidly encoded cell would raise Encoding::CompatibilityError when
204
+ # combined with text cells. Represent those bytes instead of raising.
205
+ def csv_compatible(value) # :nodoc:
206
+ return value if value.ascii_only?
207
+ return value if value.valid_encoding? && value.encoding != Encoding::BINARY
208
+
209
+ value.dup.force_encoding(Encoding::UTF_8).scrub('?')
210
+ end
211
+
179
212
  def version_config
180
213
  @version_config ||= VersionConfig.new(version)
181
214
  end
data/lib/dbf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DBF
4
- VERSION = '5.3.0'
4
+ VERSION = '5.4.0'
5
5
  end
@@ -68,11 +68,22 @@ module DBF
68
68
  end
69
69
  end
70
70
 
71
+ # Returns the Column.new arguments for the next column descriptor, or nil
72
+ # when the file ends mid-descriptor. Unpacking a short descriptor would
73
+ # otherwise raise ArgumentError ("x outside of string").
71
74
  def read_column_args(table, io)
75
+ size, format, defaults = column_layout
76
+ data = io.read(size)
77
+ return nil unless data && data.bytesize == size
78
+
79
+ [table, *data.unpack(format), *defaults]
80
+ end
81
+
82
+ def column_layout # :nodoc:
72
83
  case version
73
- when '02' then [table, *io.read(header_size * 2).unpack('A11 a C'), 0]
74
- when '04', '8c' then [table, *io.read(48).unpack('A32 a C C x13')]
75
- else [table, *io.read(header_size).unpack('A11 a x4 C2')]
84
+ when '02' then [header_size * 2, 'A11 a C', [0]]
85
+ when '04', '8c' then [48, 'A32 a C C x13', []]
86
+ else [header_size, 'A11 a x4 C2', []]
76
87
  end
77
88
  end
78
89
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbf
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Morrison
@@ -79,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
81
  requirements: []
82
- rubygems_version: 4.0.10
82
+ rubygems_version: 4.0.17
83
83
  specification_version: 4
84
84
  summary: Read xBase files
85
85
  test_files: []