dbf 5.2.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +15 -7
- data/bin/dbf +2 -54
- data/lib/dbf/cli.rb +154 -0
- data/lib/dbf/column.rb +11 -3
- data/lib/dbf/column_builder.rb +15 -2
- data/lib/dbf/column_type.rb +22 -9
- data/lib/dbf/database/foxpro.rb +16 -2
- data/lib/dbf/encoder.rb +116 -0
- data/lib/dbf/encodings.rb +8 -0
- data/lib/dbf/header.rb +5 -0
- data/lib/dbf/memo/dbase3.rb +6 -1
- data/lib/dbf/memo/dbase4.rb +15 -1
- data/lib/dbf/memo/foxpro.rb +9 -5
- data/lib/dbf/record.rb +3 -1
- data/lib/dbf/record_iterator.rb +14 -2
- data/lib/dbf/schema.rb +4 -4
- data/lib/dbf/table.rb +44 -6
- data/lib/dbf/version.rb +1 -1
- data/lib/dbf/version_config.rb +14 -3
- data/lib/dbf.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f296a6fc75892231410af21b2ee94490666f740f425f671078b5c28ded1e2977
|
|
4
|
+
data.tar.gz: eed4dfd84582cdf9c8e125ca2ddb1e882ebe75147321469db26166f7777a1ea1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 578f0f6cc7c88a205c38fd2ef93af2d8e5c924483018af4b96fb349dd12f40386c13547dfc67cd6fbdd47e191a0d04ef46bf0e1c38f9baec6057197661cd6945
|
|
7
|
+
data.tar.gz: 6960aba5a5b253cc6041c0822a504714ab5a511fee4ea1ff84f43cde10c8cf7db4d74d46f84e670d49f3a4d7110525a3196ea2f76cbcc37174a657e56a755fae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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
|
|
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
|
|
25
|
+
- Blank Visual FoxPro "T" (DateTime) columns now return nil instead of a Julian day-0 date
|
|
26
|
+
- Blank "F" (Float) columns now return nil instead of 0.0, matching "N" (Number) behavior
|
|
27
|
+
- Extract CLI logic into DBF::CLI class (bin/dbf is now a thin shim)
|
|
28
|
+
- DBF::Table#to_csv now accepts an IO in addition to a String path
|
|
29
|
+
- Fix broken link in README
|
|
30
|
+
|
|
3
31
|
## 5.2.0
|
|
4
32
|
|
|
5
33
|
- Drop support for Ruby 3.1 and 3.2
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
DBF is a small, fast Ruby library for reading dBase, xBase, Clipper, and FoxPro database files.
|
|
11
11
|
|
|
12
12
|
* Project page: <https://github.com/infused/dbf>
|
|
13
|
-
* API Documentation: <https://
|
|
13
|
+
* API Documentation: <https://rdoc.info/gems/dbf>
|
|
14
14
|
* Report bugs: <https://github.com/infused/dbf/issues>
|
|
15
15
|
* Questions: Email <mailto:keithm@infused.org> and put DBF somewhere in the
|
|
16
16
|
subject line
|
|
@@ -21,7 +21,7 @@ NOTE: Beginning with version 5.2 we have dropped support for Ruby 3.2 and earlie
|
|
|
21
21
|
NOTE: Beginning with version 4.3 we have dropped support for Ruby 3.0 and earlier.
|
|
22
22
|
|
|
23
23
|
NOTE: Beginning with version 4 we have dropped support for Ruby 2.0, 2.1, 2.2, and 2.3. If you need support for these older Rubies,
|
|
24
|
-
please use 3.0.x (<https://github.com/infused
|
|
24
|
+
please use 3.0.x (<https://github.com/infused/dbf/tree/3_stable>)
|
|
25
25
|
|
|
26
26
|
NOTE: Beginning with version 3 we have dropped support for Ruby 1.8 and 1.9. If you need support for older Rubies,
|
|
27
27
|
please use 2.0.x (<https://github.com/infused/dbf/tree/2_stable>)
|
|
@@ -151,6 +151,7 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
151
151
|
| 01 | cp437 | U.S. MS–DOS |
|
|
152
152
|
| 02 | cp850 | International MS–DOS |
|
|
153
153
|
| 03 | cp1252 | Windows ANSI |
|
|
154
|
+
| 04 | macRoman | Standard Macintosh |
|
|
154
155
|
| 08 | cp865 | Danish OEM |
|
|
155
156
|
| 09 | cp437 | Dutch OEM |
|
|
156
157
|
| 0a | cp850 | Dutch OEM* |
|
|
@@ -191,6 +192,8 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
191
192
|
| 65 | cp866 | Russian MS–DOS |
|
|
192
193
|
| 66 | cp865 | Nordic MS–DOS |
|
|
193
194
|
| 67 | cp861 | Icelandic MS–DOS |
|
|
195
|
+
| 68 | cp895 | Kamenický (Czech) MS–DOS |
|
|
196
|
+
| 69 | cp620 | Mazovia (Polish) MS–DOS |
|
|
194
197
|
| 6a | cp737 | Greek MS–DOS (437G) |
|
|
195
198
|
| 6b | cp857 | Turkish MS–DOS |
|
|
196
199
|
| 6c | cp863 | French–Canadian MS–DOS |
|
|
@@ -199,9 +202,14 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
199
202
|
| 7a | cp936 | PRC GBK |
|
|
200
203
|
| 7b | cp932 | Japanese Shift-JIS |
|
|
201
204
|
| 7c | cp874 | Thai Windows/MS–DOS |
|
|
205
|
+
| 7d | cp1255 | Hebrew Windows |
|
|
206
|
+
| 7e | cp1256 | Arabic Windows |
|
|
202
207
|
| 86 | cp737 | Greek OEM |
|
|
203
208
|
| 87 | cp852 | Slovenian OEM |
|
|
204
209
|
| 88 | cp857 | Turkish OEM |
|
|
210
|
+
| 96 | macCyrillic | Russian Macintosh |
|
|
211
|
+
| 97 | macCentEuro | Macintosh EE |
|
|
212
|
+
| 98 | macGreek | Greek Macintosh |
|
|
205
213
|
| c8 | cp1250 | Eastern European Windows |
|
|
206
214
|
| c9 | cp1251 | Russian Windows |
|
|
207
215
|
| ca | cp1254 | Turkish Windows |
|
|
@@ -328,16 +336,16 @@ at this time.
|
|
|
328
336
|
| 04 | dBase IV without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
329
337
|
| 05 | dBase V without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
330
338
|
| 07 | Visual Objects 1.x | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
331
|
-
| 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
332
|
-
| 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
333
|
-
| 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
339
|
+
| 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | - |
|
|
340
|
+
| 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
341
|
+
| 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
334
342
|
| 7b | dBase IV with memo file | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - |
|
|
335
343
|
| 83 | dBase III with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
336
344
|
| 87 | Visual Objects 1.x with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
337
345
|
| 8b | dBase IV with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
|
|
338
346
|
| 8e | dBase IV with SQL table | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
|
|
339
|
-
| f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
340
|
-
| fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y |
|
|
347
|
+
| f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
348
|
+
| fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
341
349
|
|
|
342
350
|
Data type descriptions
|
|
343
351
|
|
data/bin/dbf
CHANGED
|
@@ -1,60 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
Signal.trap('PIPE', 'SYSTEM_DEFAULT')
|
|
4
5
|
|
|
5
6
|
require 'dbf'
|
|
6
|
-
require 'dbf/version'
|
|
7
|
-
require 'optparse'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if params['v']
|
|
12
|
-
puts "dbf version: #{DBF::VERSION}"
|
|
13
|
-
|
|
14
|
-
elsif params['h']
|
|
15
|
-
puts "usage: #{File.basename(__FILE__)} [-h|-s|-a|-c|-r] filename"
|
|
16
|
-
puts ' -h = print this message'
|
|
17
|
-
puts ' -v = print the DBF gem version'
|
|
18
|
-
puts ' -s = print summary information'
|
|
19
|
-
puts ' -a = create an ActiveRecord::Schema'
|
|
20
|
-
puts ' -r = create a Sequel migration'
|
|
21
|
-
puts ' -c = export as CSV'
|
|
22
|
-
else
|
|
23
|
-
|
|
24
|
-
filename = ARGV.shift
|
|
25
|
-
abort 'You must supply a filename on the command line' unless filename
|
|
26
|
-
|
|
27
|
-
# create an ActiveRecord::Schema
|
|
28
|
-
if params['a']
|
|
29
|
-
table = DBF::Table.new filename
|
|
30
|
-
puts table.schema(:activerecord)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# create an Sequel::Migration
|
|
34
|
-
if params['r']
|
|
35
|
-
table = DBF::Table.new filename
|
|
36
|
-
puts table.schema(:sequel)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
if params['s']
|
|
40
|
-
table = DBF::Table.new filename
|
|
41
|
-
puts
|
|
42
|
-
puts "Database: #{filename}"
|
|
43
|
-
puts "Type: (#{table.version}) #{table.version_description}"
|
|
44
|
-
puts "Encoding: #{table.header_encoding}" if table.header_encoding
|
|
45
|
-
puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}"
|
|
46
|
-
puts "Records: #{table.record_count}"
|
|
47
|
-
|
|
48
|
-
puts "\nFields:"
|
|
49
|
-
puts 'Name Type Length Decimal'
|
|
50
|
-
puts '-' * 78
|
|
51
|
-
table.columns.each do |f|
|
|
52
|
-
puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
if params['c']
|
|
57
|
-
table = DBF::Table.new filename
|
|
58
|
-
table.to_csv
|
|
59
|
-
end
|
|
60
|
-
end
|
|
8
|
+
exit DBF::CLI.run(ARGV)
|
data/lib/dbf/cli.rb
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module DBF
|
|
6
|
+
class CLI
|
|
7
|
+
USAGE = <<~HELP
|
|
8
|
+
usage: dbf [-h|-s|-a|-c|-r] filename
|
|
9
|
+
-h = print this message
|
|
10
|
+
-v = print the DBF gem version
|
|
11
|
+
-s = print summary information
|
|
12
|
+
-a = create an ActiveRecord::Schema
|
|
13
|
+
-r = create a Sequel migration
|
|
14
|
+
-c = export as CSV
|
|
15
|
+
HELP
|
|
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
|
+
|
|
61
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
62
|
+
new(argv, stdout: stdout, stderr: stderr).run
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
66
|
+
@argv = argv.dup
|
|
67
|
+
@stdout = stdout
|
|
68
|
+
@stderr = stderr
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def run
|
|
72
|
+
params = OptionParser.new.getopts(@argv, 'h', 's', 'a', 'c', 'r', 'v')
|
|
73
|
+
|
|
74
|
+
if params['v']
|
|
75
|
+
print_version
|
|
76
|
+
elsif params['h']
|
|
77
|
+
print_help
|
|
78
|
+
else
|
|
79
|
+
filename = @argv.shift
|
|
80
|
+
return missing_filename unless filename
|
|
81
|
+
|
|
82
|
+
action = %w[a r s c].find { |flag| params[flag] }
|
|
83
|
+
case action
|
|
84
|
+
when 'a' then print_ar_schema(filename)
|
|
85
|
+
when 'r' then print_sequel_schema(filename)
|
|
86
|
+
when 's' then print_summary(filename)
|
|
87
|
+
when 'c' then print_csv(filename)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
0
|
|
91
|
+
rescue DBF::FileNotFoundError => e
|
|
92
|
+
@stderr.puts "DBF::FileNotFoundError: #{e.message}"
|
|
93
|
+
1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def print_version
|
|
99
|
+
@stdout.puts "dbf version: #{DBF::VERSION}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_help
|
|
103
|
+
@stdout.puts USAGE
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def missing_filename
|
|
107
|
+
@stderr.puts 'You must supply a filename on the command line'
|
|
108
|
+
1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def print_ar_schema(filename)
|
|
112
|
+
@stdout.puts terminal_safe(DBF::Table.new(filename).schema(:activerecord))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_sequel_schema(filename)
|
|
116
|
+
@stdout.puts terminal_safe(DBF::Table.new(filename).schema(:sequel))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def print_summary(filename)
|
|
120
|
+
table = DBF::Table.new(filename)
|
|
121
|
+
@stdout.puts
|
|
122
|
+
@stdout.puts "Database: #{filename}"
|
|
123
|
+
@stdout.puts "Type: (#{table.version}) #{table.version_description}"
|
|
124
|
+
@stdout.puts "Encoding: #{table.header_encoding}" if table.header_encoding
|
|
125
|
+
@stdout.puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}"
|
|
126
|
+
@stdout.puts "Records: #{table.record_count}"
|
|
127
|
+
@stdout.puts "\nFields:"
|
|
128
|
+
@stdout.puts 'Name Type Length Decimal'
|
|
129
|
+
@stdout.puts '-' * 78
|
|
130
|
+
table.columns.each do |f|
|
|
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)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def print_csv(filename)
|
|
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?
|
|
152
|
+
end
|
|
153
|
+
end
|
|
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
|
|
|
@@ -58,8 +58,8 @@ module DBF
|
|
|
58
58
|
# @param raw [String]
|
|
59
59
|
# @yield [raw] for memo column resolution
|
|
60
60
|
# @return decoded value
|
|
61
|
-
def decode(raw, &
|
|
62
|
-
type_cast_class.decode(raw, &
|
|
61
|
+
def decode(raw, &)
|
|
62
|
+
type_cast_class.decode(raw, &)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Returns a Hash with :name, :type, :length, and :decimal keys
|
|
@@ -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]
|
data/lib/dbf/column_builder.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
data/lib/dbf/column_type.rb
CHANGED
|
@@ -5,8 +5,7 @@ module DBF
|
|
|
5
5
|
class Base
|
|
6
6
|
attr_reader :decimal, :encoding
|
|
7
7
|
|
|
8
|
-
# @param
|
|
9
|
-
# @param encoding [String, Encoding]
|
|
8
|
+
# @param column [DBF::Column]
|
|
10
9
|
def initialize(column)
|
|
11
10
|
@decimal = column.decimal
|
|
12
11
|
@encoding = column.encoding
|
|
@@ -20,7 +19,11 @@ module DBF
|
|
|
20
19
|
false
|
|
21
20
|
end
|
|
22
21
|
|
|
23
|
-
def decode(raw, &
|
|
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
|
+
|
|
24
27
|
if skip_blank? && raw.count(' ') == raw.length
|
|
25
28
|
blank_value
|
|
26
29
|
else
|
|
@@ -50,7 +53,8 @@ module DBF
|
|
|
50
53
|
class Currency < Base
|
|
51
54
|
# @param value [String]
|
|
52
55
|
def type_cast(value)
|
|
53
|
-
|
|
56
|
+
int = value.unpack1('q<')
|
|
57
|
+
int && (int / 10_000.0).to_f
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
@@ -65,14 +69,20 @@ module DBF
|
|
|
65
69
|
# @param value [String]
|
|
66
70
|
def type_cast(value)
|
|
67
71
|
bits = value.unpack1('B*')
|
|
72
|
+
return nil unless bits && bits.length >= 32
|
|
73
|
+
|
|
68
74
|
sign_multiplier = bits[0] == '0' ? -1 : 1
|
|
69
75
|
bits[1, 31].to_i(2) * sign_multiplier
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
class Float < Base
|
|
80
|
+
def skip_blank? = true
|
|
81
|
+
|
|
74
82
|
# @param value [String]
|
|
75
83
|
def type_cast(value)
|
|
84
|
+
return nil if value.empty?
|
|
85
|
+
|
|
76
86
|
value.to_f
|
|
77
87
|
end
|
|
78
88
|
end
|
|
@@ -111,6 +121,8 @@ module DBF
|
|
|
111
121
|
# @param value [String]
|
|
112
122
|
def type_cast(value)
|
|
113
123
|
days, msecs = value.unpack('l2')
|
|
124
|
+
return nil if days.nil? || days.zero?
|
|
125
|
+
|
|
114
126
|
secs = (msecs / 1000).to_i
|
|
115
127
|
::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time
|
|
116
128
|
rescue StandardError
|
|
@@ -119,8 +131,8 @@ module DBF
|
|
|
119
131
|
end
|
|
120
132
|
|
|
121
133
|
class Memo < Base
|
|
122
|
-
def decode(raw, &
|
|
123
|
-
memo_content =
|
|
134
|
+
def decode(raw, &)
|
|
135
|
+
memo_content = yield(raw)
|
|
124
136
|
memo_content ? type_cast(memo_content) : nil
|
|
125
137
|
end
|
|
126
138
|
|
|
@@ -128,7 +140,7 @@ module DBF
|
|
|
128
140
|
def type_cast(value)
|
|
129
141
|
return value unless encoding && value
|
|
130
142
|
|
|
131
|
-
|
|
143
|
+
Encoder.encode(value.dup, encoding)
|
|
132
144
|
end
|
|
133
145
|
end
|
|
134
146
|
|
|
@@ -158,8 +170,9 @@ module DBF
|
|
|
158
170
|
private
|
|
159
171
|
|
|
160
172
|
def encode(value)
|
|
161
|
-
value.force_encoding(encoding)
|
|
162
|
-
|
|
173
|
+
return value.force_encoding(encoding) unless @needs_encode
|
|
174
|
+
|
|
175
|
+
Encoder.encode(value, encoding, @target_encoding)
|
|
163
176
|
end
|
|
164
177
|
end
|
|
165
178
|
end
|
data/lib/dbf/database/foxpro.rb
CHANGED
|
@@ -47,11 +47,25 @@ module DBF
|
|
|
47
47
|
# @param name [String]
|
|
48
48
|
# @return [String]
|
|
49
49
|
def table_path(name)
|
|
50
|
-
|
|
51
|
-
|
|
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/encoder.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBF
|
|
4
|
+
# Transcodes strings from a source encoding to a target encoding.
|
|
5
|
+
#
|
|
6
|
+
# Most DBF code pages map to encodings built into Ruby and are converted
|
|
7
|
+
# with String#encode. A few (Mazovia cp620, Kamenický cp895) have no Ruby
|
|
8
|
+
# equivalent and are decoded through vendored byte-to-Unicode tables.
|
|
9
|
+
module Encoder
|
|
10
|
+
# Byte-to-Unicode tables generated from
|
|
11
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/620.TBL and
|
|
12
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/895.TBL
|
|
13
|
+
|
|
14
|
+
# Mazovia (Polish) MS-DOS
|
|
15
|
+
CP620_TO_UNICODE = [
|
|
16
|
+
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
|
|
17
|
+
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
18
|
+
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
|
|
19
|
+
0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
|
|
20
|
+
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
|
21
|
+
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
|
22
|
+
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
|
23
|
+
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
|
24
|
+
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
|
25
|
+
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
|
26
|
+
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
|
27
|
+
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
|
28
|
+
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
|
29
|
+
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
|
30
|
+
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
|
31
|
+
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
|
|
32
|
+
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x0105, 0x00E7,
|
|
33
|
+
0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x0107, 0x00C4, 0x0104,
|
|
34
|
+
0x0118, 0x0119, 0x0142, 0x00F4, 0x00F6, 0x0106, 0x00FB, 0x00F9,
|
|
35
|
+
0x015A, 0x00D6, 0x00DC, 0x00A2, 0x0141, 0x00A5, 0x015B, 0x0192,
|
|
36
|
+
0x0179, 0x017B, 0x00F3, 0x00D3, 0x0144, 0x0143, 0x017A, 0x017C,
|
|
37
|
+
0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
|
|
38
|
+
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
|
|
39
|
+
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
|
|
40
|
+
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
|
41
|
+
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
|
|
42
|
+
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
|
|
43
|
+
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
|
|
44
|
+
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
|
|
45
|
+
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
|
|
46
|
+
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248,
|
|
47
|
+
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# Kamenický (Czech) MS-DOS
|
|
51
|
+
CP895_TO_UNICODE = [
|
|
52
|
+
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
|
|
53
|
+
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
54
|
+
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
|
|
55
|
+
0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
|
|
56
|
+
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
|
57
|
+
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
|
58
|
+
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
|
59
|
+
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
|
60
|
+
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
|
61
|
+
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
|
62
|
+
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
|
63
|
+
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
|
64
|
+
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
|
65
|
+
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
|
66
|
+
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
|
67
|
+
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
|
|
68
|
+
0x010C, 0x00FC, 0x00E9, 0x010F, 0x00E4, 0x010E, 0x0164, 0x010D,
|
|
69
|
+
0x011B, 0x011A, 0x0139, 0x00CD, 0x013E, 0x01EA, 0x00C4, 0x00C1,
|
|
70
|
+
0x00C9, 0x017E, 0x017D, 0x00F4, 0x00F6, 0x00D3, 0x016F, 0x00DA,
|
|
71
|
+
0x00FD, 0x00D6, 0x00DC, 0x0160, 0x013D, 0x00DD, 0x0158, 0x0165,
|
|
72
|
+
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x0148, 0x0147, 0x016E, 0x00D4,
|
|
73
|
+
0x0161, 0x0159, 0x0155, 0x0154, 0x00BC, 0x00A7, 0x00AB, 0x00BB,
|
|
74
|
+
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
|
|
75
|
+
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
|
|
76
|
+
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
|
77
|
+
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
|
|
78
|
+
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
|
|
79
|
+
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
|
|
80
|
+
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
|
|
81
|
+
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
|
|
82
|
+
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248,
|
|
83
|
+
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
|
84
|
+
].freeze
|
|
85
|
+
|
|
86
|
+
CUSTOM_TABLES = {
|
|
87
|
+
'cp620' => CP620_TO_UNICODE,
|
|
88
|
+
'cp895' => CP895_TO_UNICODE
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
# Is the encoding a code page without a Ruby Encoding, decoded through
|
|
92
|
+
# a vendored table?
|
|
93
|
+
#
|
|
94
|
+
# @param encoding [String, Encoding, nil]
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def self.custom?(encoding)
|
|
97
|
+
encoding.is_a?(::String) && CUSTOM_TABLES.key?(encoding.downcase)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Transcode a string from source_encoding to target_encoding
|
|
101
|
+
#
|
|
102
|
+
# @param string [String]
|
|
103
|
+
# @param source_encoding [String, Encoding]
|
|
104
|
+
# @param target_encoding [Encoding]
|
|
105
|
+
# @return [String]
|
|
106
|
+
def self.encode(string, source_encoding, target_encoding = Encoding.default_external)
|
|
107
|
+
table = source_encoding.is_a?(::String) && CUSTOM_TABLES[source_encoding.downcase]
|
|
108
|
+
if table
|
|
109
|
+
decoded = string.each_byte.map { |byte| table[byte] }.pack('U*')
|
|
110
|
+
target_encoding == Encoding::UTF_8 ? decoded : decoded.encode(target_encoding, undef: :replace, invalid: :replace)
|
|
111
|
+
else
|
|
112
|
+
string.force_encoding(source_encoding).encode(target_encoding, undef: :replace, invalid: :replace)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/dbf/encodings.rb
CHANGED
|
@@ -5,6 +5,7 @@ module DBF
|
|
|
5
5
|
'01' => 'cp437', # U.S. MS-DOS
|
|
6
6
|
'02' => 'cp850', # International MS-DOS
|
|
7
7
|
'03' => 'cp1252', # Windows ANSI
|
|
8
|
+
'04' => 'macRoman', # Standard Macintosh
|
|
8
9
|
'08' => 'cp865', # Danish OEM
|
|
9
10
|
'09' => 'cp437', # Dutch OEM
|
|
10
11
|
'0a' => 'cp850', # Dutch OEM*
|
|
@@ -45,6 +46,8 @@ module DBF
|
|
|
45
46
|
'65' => 'cp866', # Russian MS-DOS
|
|
46
47
|
'66' => 'cp865', # Nordic MS-DOS
|
|
47
48
|
'67' => 'cp861', # Icelandic MS-DOS
|
|
49
|
+
'68' => 'cp895', # Kamenicky (Czech) MS-DOS
|
|
50
|
+
'69' => 'cp620', # Mazovia (Polish) MS-DOS
|
|
48
51
|
'6a' => 'cp737', # Greek MS-DOS (437G)
|
|
49
52
|
'6b' => 'cp857', # Turkish MS-DOS
|
|
50
53
|
'6c' => 'cp863', # French-Canadian MS-DOS
|
|
@@ -53,9 +56,14 @@ module DBF
|
|
|
53
56
|
'7a' => 'cp936', # PRC GBK
|
|
54
57
|
'7b' => 'cp932', # Japanese Shift-JIS
|
|
55
58
|
'7c' => 'cp874', # Thai Windows/MS-DOS
|
|
59
|
+
'7d' => 'cp1255', # Hebrew Windows
|
|
60
|
+
'7e' => 'cp1256', # Arabic Windows
|
|
56
61
|
'86' => 'cp737', # Greek OEM
|
|
57
62
|
'87' => 'cp852', # Slovenian OEM
|
|
58
63
|
'88' => 'cp857', # Turkish OEM
|
|
64
|
+
'96' => 'macCyrillic', # Russian Macintosh
|
|
65
|
+
'97' => 'macCentEuro', # Macintosh EE
|
|
66
|
+
'98' => 'macGreek', # Greek Macintosh
|
|
59
67
|
'c8' => 'cp1250', # Eastern European Windows
|
|
60
68
|
'c9' => 'cp1251', # Russian Windows
|
|
61
69
|
'ca' => 'cp1254', # Turkish Windows
|
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
|
data/lib/dbf/memo/dbase3.rb
CHANGED
|
@@ -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)
|
|
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
|
data/lib/dbf/memo/dbase4.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/dbf/memo/foxpro.rb
CHANGED
|
@@ -23,11 +23,15 @@ module DBF
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def read_memo_content(memo_string, memo_size) # :nodoc:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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'
|
data/lib/dbf/record_iterator.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
|
@@ -140,11 +148,16 @@ module DBF
|
|
|
140
148
|
# Dumps all records to a CSV file. If no filename is given then CSV is
|
|
141
149
|
# output to STDOUT.
|
|
142
150
|
#
|
|
143
|
-
# @param [optional String] path
|
|
144
|
-
def to_csv(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
# @param [optional String, IO] path_or_io String path, IO-like object, or nil for STDOUT
|
|
152
|
+
def to_csv(path_or_io = nil)
|
|
153
|
+
io = case path_or_io
|
|
154
|
+
when nil then $stdout
|
|
155
|
+
when String then File.open(path_or_io, 'w')
|
|
156
|
+
else path_or_io
|
|
157
|
+
end
|
|
158
|
+
csv = CSV.new(io, force_quotes: true)
|
|
159
|
+
csv << column_names.map { |name| csv_safe_value(name) }
|
|
160
|
+
each { |record| csv << record.to_a.map { |value| csv_safe_value(value) } }
|
|
148
161
|
end
|
|
149
162
|
|
|
150
163
|
# Human readable version description
|
|
@@ -159,7 +172,7 @@ module DBF
|
|
|
159
172
|
# @param [String] string
|
|
160
173
|
# @return [String]
|
|
161
174
|
def encode_string(string) # :nodoc:
|
|
162
|
-
|
|
175
|
+
Encoder.encode(string, @encoding)
|
|
163
176
|
end
|
|
164
177
|
|
|
165
178
|
# Encoding specified in the file header
|
|
@@ -171,6 +184,31 @@ module DBF
|
|
|
171
184
|
|
|
172
185
|
private
|
|
173
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
|
+
|
|
174
212
|
def version_config
|
|
175
213
|
@version_config ||= VersionConfig.new(version)
|
|
176
214
|
end
|
data/lib/dbf/version.rb
CHANGED
data/lib/dbf/version_config.rb
CHANGED
|
@@ -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 [
|
|
74
|
-
when '04', '8c' then [
|
|
75
|
-
else [
|
|
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
|
data/lib/dbf.rb
CHANGED
|
@@ -14,6 +14,7 @@ require 'dbf/record_context'
|
|
|
14
14
|
require 'dbf/column_type'
|
|
15
15
|
require 'dbf/column'
|
|
16
16
|
require 'dbf/encodings'
|
|
17
|
+
require 'dbf/encoder'
|
|
17
18
|
require 'dbf/header'
|
|
18
19
|
require 'dbf/version_config'
|
|
19
20
|
require 'dbf/file_handler'
|
|
@@ -25,3 +26,4 @@ require 'dbf/memo/dbase3'
|
|
|
25
26
|
require 'dbf/memo/dbase4'
|
|
26
27
|
require 'dbf/memo/foxpro'
|
|
27
28
|
require 'dbf/database/foxpro'
|
|
29
|
+
require 'dbf/cli'
|
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.
|
|
4
|
+
version: 5.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keith Morrison
|
|
@@ -37,10 +37,12 @@ files:
|
|
|
37
37
|
- bin/dbf
|
|
38
38
|
- dbf.gemspec
|
|
39
39
|
- lib/dbf.rb
|
|
40
|
+
- lib/dbf/cli.rb
|
|
40
41
|
- lib/dbf/column.rb
|
|
41
42
|
- lib/dbf/column_builder.rb
|
|
42
43
|
- lib/dbf/column_type.rb
|
|
43
44
|
- lib/dbf/database/foxpro.rb
|
|
45
|
+
- lib/dbf/encoder.rb
|
|
44
46
|
- lib/dbf/encodings.rb
|
|
45
47
|
- lib/dbf/file_handler.rb
|
|
46
48
|
- lib/dbf/find.rb
|
|
@@ -77,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
79
|
- !ruby/object:Gem::Version
|
|
78
80
|
version: '0'
|
|
79
81
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
82
|
+
rubygems_version: 4.0.17
|
|
81
83
|
specification_version: 4
|
|
82
84
|
summary: Read xBase files
|
|
83
85
|
test_files: []
|