dbf 5.5.0 → 5.6.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 +32 -1
- data/README.md +40 -0
- data/lib/dbf/cli.rb +9 -3
- data/lib/dbf/column.rb +20 -2
- data/lib/dbf/column_builder.rb +38 -2
- data/lib/dbf/column_type.rb +9 -2
- data/lib/dbf/database/foxpro.rb +22 -12
- data/lib/dbf/encoder.rb +60 -5
- data/lib/dbf/errors.rb +24 -1
- data/lib/dbf/file_handler.rb +46 -9
- data/lib/dbf/memo/base.rb +15 -4
- data/lib/dbf/memo/dbase3.rb +18 -4
- data/lib/dbf/memo/dbase4.rb +1 -1
- data/lib/dbf/memo/foxpro.rb +1 -1
- data/lib/dbf/record.rb +17 -2
- data/lib/dbf/table.rb +30 -5
- data/lib/dbf/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9d4250b08de3a7a96f24fff3f55e5457524288951b9d0ca8ffc8127aae2c790
|
|
4
|
+
data.tar.gz: 78d946bd48943b8d6527aac3e982017133480d8263bc5025d2c8d3a85407185e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2e613d19239abb6df53c941c952da129bb77ff6cfbcc7b91cdeac10d9f8ad28724102c20ad6f53533959e0adc1f0f159ffadd0811f4997b623633d5c9fa2edf
|
|
7
|
+
data.tar.gz: 5964675e521e61be0d3da141c642cfc13c0e71b232ae2a4070053d0764b3e3ee1ca653a786ef70f3001b98f24b4eaf05eee9c4a81a86fd14b2506a939ea2eba3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.6.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Security (CWE-200, CWE-400): memo auto-discovery now looks only for a memo with exactly the table's basename (`.fpt`/`.dbt`, any case), by scanning the directory rather than globbing. Previously `1.dbf` could pick up `12.fpt`, and glob metacharacters in an uploaded filename were expanded
|
|
8
|
+
- Security (CWE-400): column parsing stops at `header_length` and drops columns that do not fit within `record_length`, so a small crafted file can no longer declare thousands of overlapping columns and cause quadratic decoding work and output
|
|
9
|
+
- Security (CWE-838): string values that need no transcoding have invalid bytes replaced, as transcoded values already did, so they no longer raise in JSON generation, HTML escaping, or database inserts
|
|
10
|
+
- Security (CWE-248): a DateTime (`T`) column narrower than 8 bytes decodes to nil instead of raising `NoMethodError`
|
|
11
|
+
- Security (CWE-915): column names (and Visual FoxPro `.dbc` table names) such as `to_str`, `to_ary`, `to_hash`, `to_proc`, or `to_partial_path` are no longer exposed as methods, so a crafted file cannot hijack implicit conversions or Rails rendering; those columns stay readable via `#[]` and `#attributes`
|
|
12
|
+
- Security (CWE-129): `Table#record` returns nil for a negative index instead of reading header bytes as a record or raising `Errno::EINVAL`
|
|
13
|
+
- Security (CWE-150): CLI terminal output also replaces UTF-8 encoded C1 control characters (U+0080–U+009F, e.g. the single-character CSI U+009B)
|
|
14
|
+
- Security (CWE-400): a table declares no more columns than its records have bytes, so zero-length columns can no longer bypass the column bound (an 84 KB file previously produced 2,000 nil cells per one-byte record: a 120 MB CSV and 34 s of work)
|
|
15
|
+
- Security (CWE-248): a dBase III/IV memo pointer too large to seek to decodes to nil instead of raising `RangeError`
|
|
16
|
+
- Tables with the Macintosh Central European language driver (`0x97`) decode through a vendored code page table instead of raising `Encoding::ConverterNotFoundError`, since Ruby has no converter for `macCentEuro`
|
|
17
|
+
- dBase III memos end at the `0x1A` terminator; bytes left over after it in the same block are no longer appended to the memo
|
|
18
|
+
- Visual FoxPro long column names from a `.dbc` container are no longer decoded a second time with the table's code page, which garbled non-ASCII names (`Café` became `Café`)
|
|
19
|
+
- Security (CWE-248): `Table.new` raises only `DBF::Error` subclasses for a bad path or encoding, so rescuing `DBF::Error` is enough. A directory or unreadable file (data or memo) raises the new `DBF::FileAccessError` instead of `Errno::EISDIR`/`Errno::EACCES`. A missing memo path, or a path containing a null byte, raises `DBF::FileNotFoundError` instead of `Errno::ENOENT`/`ArgumentError`. An unknown encoding, or one Ruby cannot transcode such as UTF-7, raises the new `DBF::UnsupportedEncodingError` when the table is opened, rather than `ArgumentError` or `Encoding::ConverterNotFoundError` on the first read. The data file is closed if its memo cannot be opened
|
|
20
|
+
- Security (CWE-209): `DBF::FileNotFoundError` and `DBF::FileAccessError` messages name only the file (`file not found: data.dbf`), not its directory, so an app that shows error messages to users does not reveal server paths. The full path is available as `#path`. Both now inherit from the new `DBF::FileError`
|
|
21
|
+
- The `dbf` command reports a file that cannot be opened (such as a directory) as an error and exits 1, instead of printing a stack trace
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Code that rescued `ArgumentError`, `Errno::*` or `Encoding::ConverterNotFoundError` from `Table.new` should rescue `DBF::Error` (or the specific subclasses above) instead. Passing something other than a path or IO still raises `ArgumentError`
|
|
26
|
+
- File error messages no longer include the directory (`file not found: data.dbf` rather than `file not found: /srv/uploads/data.dbf`). Use `DBF::FileError#path` for the full path
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Security (CWE-400): optional `max_memo_size:` for `DBF::Table.new`, `DBF::Table.open` and `DBF::Database::Foxpro.new`. Each memo is cut off after that many bytes, and no more than that is read from the memo file, so a small table whose records all point at one large (or unterminated) memo can no longer produce output far larger than the upload. Memos are read in full by default
|
|
31
|
+
- `rake fuzz`: coverage-guided mutation fuzzer for the read path (`spec/fuzz/fuzz.rb`), also run as a non-blocking CI job
|
|
32
|
+
- A warning when column descriptors are ignored because they do not fit within the header length or record length, so a file written with wrong length fields no longer loses columns silently. The warning holds no text from the file, and is silenced when `$VERBOSE` is nil
|
|
33
|
+
- README: "Processing untrusted files" section with guidance for accepting DBF uploads, including how to strip Unicode bidirectional controls and zero-width characters, which the library passes through
|
|
34
|
+
|
|
3
35
|
## 5.5.0
|
|
4
36
|
|
|
5
37
|
### Added
|
|
@@ -15,7 +47,6 @@
|
|
|
15
47
|
- Docs site (dbf.infused.org): YARD docs are now built and deployed by CI instead of being committed to the repository; the Pages workflow's deprecated v2/v3 actions (whose artifact backend was shut down in early 2025) are updated to current versions, repairing the broken deploy
|
|
16
48
|
- Dependabot updates for GitHub Actions and gems (weekly), a security policy (SECURITY.md) with private vulnerability reporting, and contributor documentation (CONTRIBUTING.md)
|
|
17
49
|
- Automated releases: pushing a `vX.Y.Z` tag now builds and publishes the gem to RubyGems.org from CI via Trusted Publishing (OIDC) — no long-lived API key. `bundler/gem_tasks` is loaded in the Rakefile, providing `rake build` / `rake release`. Git tags resume at v5.4.0; versions 4.0.0–5.3.0 were released without tags
|
|
18
|
-
|
|
19
50
|
- Ractor safety: all library constants are verified Ractor-shareable and reading/exporting tables inside non-main Ractors is covered by specs
|
|
20
51
|
- CLI: `-j` exports records as a JSON array and `-J` as JSON Lines (one record per line, streamed in constant memory); deleted records are skipped, control bytes are escaped by JSON itself, and binary cells are scrubbed instead of raising
|
|
21
52
|
|
data/README.md
CHANGED
|
@@ -398,6 +398,46 @@ Data type descriptions
|
|
|
398
398
|
* O = Double
|
|
399
399
|
* + = Autoincrement
|
|
400
400
|
|
|
401
|
+
## Processing untrusted files
|
|
402
|
+
|
|
403
|
+
The parser is hardened against malformed files: crafted headers cannot force
|
|
404
|
+
huge allocations, unbounded loops, path traversal, or crashes from truncated
|
|
405
|
+
data. If you accept DBF uploads (for example in a web application), also:
|
|
406
|
+
|
|
407
|
+
* Treat every column name and value as untrusted text. HTML-escape it before
|
|
408
|
+
rendering it, and escape JSON before embedding it in a `<script>` tag.
|
|
409
|
+
* Pass the upload as a `StringIO` (with an explicit memo `StringIO` if there is
|
|
410
|
+
one) rather than as a path, so memo files are never looked up in a shared
|
|
411
|
+
upload directory.
|
|
412
|
+
* Limit upload size and processing time. Many records can point at the same
|
|
413
|
+
large memo, so exported output can be far larger than the uploaded files.
|
|
414
|
+
Pass `max_memo_size:` to cut off each memo after that many bytes:
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
DBF::Table.new(upload, memo_upload, max_memo_size: 64 * 1024)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`DBF::Database::Foxpro.new` takes the same option.
|
|
421
|
+
* Rescue `DBF::Error`, and also `StandardError` if a malformed file must never
|
|
422
|
+
fail a request.
|
|
423
|
+
* `Table#to_csv` prefixes a `'` to string cells that begin with `=`, `+`, `-`,
|
|
424
|
+
`@`, tab, or carriage return so spreadsheets do not run them as formulas.
|
|
425
|
+
* Text is passed through as it is, including Unicode bidirectional controls
|
|
426
|
+
(such as U+202E RIGHT-TO-LEFT OVERRIDE) and zero-width characters. These
|
|
427
|
+
make displayed text differ from the underlying data (in a web page, a CSV
|
|
428
|
+
export, or `dbf` terminal output), but some real data needs them, so the
|
|
429
|
+
library does not remove them. Strip or flag them yourself where that
|
|
430
|
+
matters:
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
value.gsub(/[----]/, '')
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
* Column descriptors that do not fit within the header length or record
|
|
437
|
+
length are ignored, and the library prints a warning (silenced when
|
|
438
|
+
`$VERBOSE` is nil). If a file you trust loses columns, its header has wrong
|
|
439
|
+
length fields.
|
|
440
|
+
|
|
401
441
|
## Limitations
|
|
402
442
|
|
|
403
443
|
* DBF is read-only
|
data/lib/dbf/cli.rb
CHANGED
|
@@ -22,6 +22,9 @@ module DBF
|
|
|
22
22
|
CONTROL_BYTES = /[\x00-\x1F\x7F]/n
|
|
23
23
|
# The same, but keeping CR and LF so CSV row separators survive.
|
|
24
24
|
CONTROL_BYTES_KEEPING_NEWLINES = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/n
|
|
25
|
+
# C1 controls (U+0080..U+009F) as UTF-8 bytes. Some terminals honor
|
|
26
|
+
# these, e.g. U+009B as a single-character CSI.
|
|
27
|
+
UTF8_C1_CONTROLS = /\xC2[\x80-\x9F]/n
|
|
25
28
|
|
|
26
29
|
# Replaces terminal control bytes. Substitution happens on a byte copy so
|
|
27
30
|
# a value whose bytes are invalid in its encoding cannot raise here.
|
|
@@ -31,7 +34,10 @@ module DBF
|
|
|
31
34
|
# @return [String]
|
|
32
35
|
def self.sanitize(value, pattern = CONTROL_BYTES)
|
|
33
36
|
string = value.to_s
|
|
34
|
-
|
|
37
|
+
encoding = string.encoding
|
|
38
|
+
bytes = string.b.gsub(pattern, '?')
|
|
39
|
+
bytes = bytes.gsub(UTF8_C1_CONTROLS, '?') if encoding == Encoding::UTF_8
|
|
40
|
+
bytes.force_encoding(encoding)
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
# Wraps an IO so text written to an interactive terminal is stripped of
|
|
@@ -94,8 +100,8 @@ module DBF
|
|
|
94
100
|
send(action, filename) if action
|
|
95
101
|
end
|
|
96
102
|
0
|
|
97
|
-
rescue DBF::
|
|
98
|
-
@stderr.puts "
|
|
103
|
+
rescue DBF::FileError => e
|
|
104
|
+
@stderr.puts "#{e.class}: #{e.message}"
|
|
99
105
|
1
|
|
100
106
|
end
|
|
101
107
|
|
data/lib/dbf/column.rb
CHANGED
|
@@ -15,7 +15,7 @@ module DBF
|
|
|
15
15
|
|
|
16
16
|
attr_reader :name, :type, :length, :decimal
|
|
17
17
|
|
|
18
|
-
# rubocop:disable Style/MutableConstant
|
|
18
|
+
# rubocop:disable-next Style/MutableConstant
|
|
19
19
|
TYPE_CAST_CLASS = {
|
|
20
20
|
N: ColumnType::Number,
|
|
21
21
|
I: ColumnType::SignedLong,
|
|
@@ -29,7 +29,6 @@ module DBF
|
|
|
29
29
|
G: ColumnType::General,
|
|
30
30
|
:+ => ColumnType::AutoIncrement
|
|
31
31
|
}
|
|
32
|
-
# rubocop:enable Style/MutableConstant
|
|
33
32
|
TYPE_CAST_CLASS.default = ColumnType::String
|
|
34
33
|
TYPE_CAST_CLASS.freeze
|
|
35
34
|
|
|
@@ -53,6 +52,17 @@ module DBF
|
|
|
53
52
|
|
|
54
53
|
def encoding = @table.encoding
|
|
55
54
|
|
|
55
|
+
# Returns a copy of the column with a different name. The name is used
|
|
56
|
+
# as given rather than decoded from the table's code page, so it must
|
|
57
|
+
# already be a decoded string, such as a Visual FoxPro long name read
|
|
58
|
+
# from the .dbc container.
|
|
59
|
+
#
|
|
60
|
+
# @param name [String]
|
|
61
|
+
# @return [DBF::Column]
|
|
62
|
+
def with_name(name) # :nodoc:
|
|
63
|
+
dup.tap { |column| column.rename(name) }
|
|
64
|
+
end
|
|
65
|
+
|
|
56
66
|
# @param value [String]
|
|
57
67
|
def type_cast(value)
|
|
58
68
|
type_cast_class.type_cast(value)
|
|
@@ -84,6 +94,14 @@ module DBF
|
|
|
84
94
|
@underscored_name ||= name.gsub(/([a-z\d])([A-Z])/, '\1_\2').tr('-', '_').downcase
|
|
85
95
|
end
|
|
86
96
|
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
def rename(name) # :nodoc:
|
|
100
|
+
@name = name
|
|
101
|
+
@underscored_name = nil
|
|
102
|
+
validate_name
|
|
103
|
+
end
|
|
104
|
+
|
|
87
105
|
private
|
|
88
106
|
|
|
89
107
|
def clean(value) # :nodoc:
|
data/lib/dbf/column_builder.rb
CHANGED
|
@@ -2,22 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
module DBF
|
|
4
4
|
class ColumnBuilder
|
|
5
|
-
def initialize(table, data, version_config)
|
|
5
|
+
def initialize(table, data, version_config, header_length:, record_length:)
|
|
6
6
|
@table = table
|
|
7
7
|
@data = data
|
|
8
8
|
@version_config = version_config
|
|
9
|
+
@header_length = header_length
|
|
10
|
+
# One byte of every record is the deletion flag
|
|
11
|
+
@max_width = record_length - 1
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def build
|
|
12
15
|
safe_seek do
|
|
13
16
|
@data.seek(@version_config.header_size)
|
|
17
|
+
width = 0
|
|
14
18
|
[].tap do |columns|
|
|
15
19
|
until end_of_record?
|
|
20
|
+
count = columns.size
|
|
21
|
+
# Descriptors live inside the header. Stop at header_length so a
|
|
22
|
+
# crafted file cannot declare thousands of columns over its
|
|
23
|
+
# record area.
|
|
24
|
+
if past_header?
|
|
25
|
+
warn_ignored(count, "header length of #{@header_length}")
|
|
26
|
+
break
|
|
27
|
+
end
|
|
28
|
+
|
|
16
29
|
args = @version_config.read_column_args(@table, @data)
|
|
17
30
|
# A descriptor truncated by EOF is returned as nil, or unpacks to
|
|
18
31
|
# a nil length; stop rather than constructing an invalid column
|
|
19
32
|
# (which would crash on nil < 0).
|
|
20
|
-
|
|
33
|
+
length = args&.[](3)
|
|
34
|
+
break if length.nil?
|
|
35
|
+
|
|
36
|
+
# Columns longer than the record would read into the following
|
|
37
|
+
# records; stop so a crafted file cannot multiply decoding work.
|
|
38
|
+
# Zero-length columns take no record bytes, so also allow at most
|
|
39
|
+
# one column per record byte.
|
|
40
|
+
width += length
|
|
41
|
+
if width > @max_width || count >= @max_width
|
|
42
|
+
warn_ignored(count, "record length of #{@max_width + 1}")
|
|
43
|
+
break
|
|
44
|
+
end
|
|
21
45
|
|
|
22
46
|
columns << Column.new(*args)
|
|
23
47
|
end
|
|
@@ -27,6 +51,18 @@ module DBF
|
|
|
27
51
|
|
|
28
52
|
private
|
|
29
53
|
|
|
54
|
+
def past_header?
|
|
55
|
+
@data.pos + @version_config.column_layout.first > @header_length
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Columns that do not fit are dropped rather than read, so say so: a file
|
|
59
|
+
# written with a wrong header or record length would otherwise lose
|
|
60
|
+
# columns silently. The message holds no text from the file, so a
|
|
61
|
+
# crafted column name cannot inject lines into a log.
|
|
62
|
+
def warn_ignored(kept, limit)
|
|
63
|
+
warn "DBF: ignored the columns after the first #{kept}: they do not fit within the #{limit} bytes"
|
|
64
|
+
end
|
|
65
|
+
|
|
30
66
|
def end_of_record?
|
|
31
67
|
safe_seek do
|
|
32
68
|
byte = @data.read(1)
|
data/lib/dbf/column_type.rb
CHANGED
|
@@ -121,7 +121,8 @@ module DBF
|
|
|
121
121
|
# @param value [String]
|
|
122
122
|
def type_cast(value)
|
|
123
123
|
days, msecs = value.unpack('l2')
|
|
124
|
-
|
|
124
|
+
# A column narrower than 8 bytes leaves msecs nil
|
|
125
|
+
return nil if days.nil? || msecs.nil? || days.zero?
|
|
125
126
|
|
|
126
127
|
secs = (msecs / 1000).to_i
|
|
127
128
|
::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time
|
|
@@ -170,7 +171,13 @@ module DBF
|
|
|
170
171
|
private
|
|
171
172
|
|
|
172
173
|
def encode(value)
|
|
173
|
-
|
|
174
|
+
unless @needs_encode
|
|
175
|
+
# No transcoding happens here, so replace invalid bytes the same
|
|
176
|
+
# way Encoder.encode does rather than handing callers a string that
|
|
177
|
+
# raises in JSON generation, HTML escaping, or a database insert.
|
|
178
|
+
value.force_encoding(encoding)
|
|
179
|
+
return value.valid_encoding? ? value : value.scrub
|
|
180
|
+
end
|
|
174
181
|
|
|
175
182
|
Encoder.encode(value, encoding, @target_encoding)
|
|
176
183
|
end
|
data/lib/dbf/database/foxpro.rb
CHANGED
|
@@ -20,13 +20,16 @@ module DBF
|
|
|
20
20
|
# contacts = db.contacts.record(0)
|
|
21
21
|
#
|
|
22
22
|
# @param path [String]
|
|
23
|
-
|
|
23
|
+
# @param max_memo_size [optional Integer] Bytes to read from each memo
|
|
24
|
+
# of the container and its tables; see DBF::Table.new
|
|
25
|
+
def initialize(path, max_memo_size: nil)
|
|
24
26
|
@path = path
|
|
25
27
|
@dirname = File.dirname(@path)
|
|
26
|
-
@
|
|
28
|
+
@max_memo_size = max_memo_size
|
|
29
|
+
@db = DBF::Table.new(@path, max_memo_size:)
|
|
27
30
|
@tables = extract_dbc_data
|
|
28
31
|
rescue Errno::ENOENT
|
|
29
|
-
raise
|
|
32
|
+
raise FileHandler.not_found(path)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def table_names
|
|
@@ -38,7 +41,7 @@ module DBF
|
|
|
38
41
|
# @param name [String]
|
|
39
42
|
# @return [DBF::Table]
|
|
40
43
|
def table(name)
|
|
41
|
-
Table.new(table_path(name), long_names: @tables[name])
|
|
44
|
+
Table.new(table_path(name), long_names: @tables[name], max_memo_size: @max_memo_size)
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
# Searches the database directory for the table's dbf file
|
|
@@ -71,15 +74,21 @@ module DBF
|
|
|
71
74
|
|
|
72
75
|
def method_missing(method, *args) # :nodoc:
|
|
73
76
|
name = method.to_s
|
|
74
|
-
|
|
77
|
+
table_method?(name) ? table(name) : super
|
|
75
78
|
end
|
|
76
79
|
|
|
77
80
|
def respond_to_missing?(method, *)
|
|
78
|
-
|
|
81
|
+
table_method?(method.to_s) || super
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
private
|
|
82
85
|
|
|
86
|
+
# Table names come from the container file, so implicit-conversion
|
|
87
|
+
# method names are never dispatched to a table (see Record).
|
|
88
|
+
def table_method?(name) # :nodoc:
|
|
89
|
+
!Record::RESERVED_METHODS.include?(name) && table_names.include?(name)
|
|
90
|
+
end
|
|
91
|
+
|
|
83
92
|
# This method extracts the data from the database container. This is
|
|
84
93
|
# just an ordinary table with a treelike structure. Field definitions
|
|
85
94
|
# are in the same order as in the linked tables but only the long name
|
|
@@ -108,17 +117,18 @@ module DBF
|
|
|
108
117
|
class Table < DBF::Table
|
|
109
118
|
attr_reader :long_names
|
|
110
119
|
|
|
111
|
-
def initialize(path, long_names:)
|
|
120
|
+
def initialize(path, long_names:, max_memo_size: nil)
|
|
112
121
|
@long_names = long_names
|
|
113
|
-
super(path)
|
|
122
|
+
super(path, max_memo_size:)
|
|
114
123
|
end
|
|
115
124
|
|
|
125
|
+
# Replaces each column's short name with its long name from the
|
|
126
|
+
# container. Long names were decoded when the .dbc was read, so they
|
|
127
|
+
# are not decoded again with this table's code page.
|
|
116
128
|
def build_columns # :nodoc:
|
|
117
|
-
# modify the column definitions to use the long names as the
|
|
118
|
-
# columnname property is readonly, recreate the column definitions
|
|
119
129
|
super.each_with_index.map do |column, index|
|
|
120
|
-
long_name = long_names[index]
|
|
121
|
-
|
|
130
|
+
long_name = long_names[index]
|
|
131
|
+
long_name ? column.with_name(long_name) : column
|
|
122
132
|
end
|
|
123
133
|
end
|
|
124
134
|
end
|
data/lib/dbf/encoder.rb
CHANGED
|
@@ -4,12 +4,15 @@ module DBF
|
|
|
4
4
|
# Transcodes strings from a source encoding to a target encoding.
|
|
5
5
|
#
|
|
6
6
|
# Most DBF code pages map to encodings built into Ruby and are converted
|
|
7
|
-
# with String#encode. A few (Mazovia cp620, Kamenický cp895
|
|
8
|
-
#
|
|
7
|
+
# with String#encode. A few (Mazovia cp620, Kamenický cp895, Macintosh
|
|
8
|
+
# Central European) have no Ruby converter and are decoded through
|
|
9
|
+
# vendored byte-to-Unicode tables.
|
|
9
10
|
module Encoder
|
|
10
11
|
# Byte-to-Unicode tables generated from
|
|
11
|
-
# https://github.com/SheetJS/js-codepage/blob/master/codepages/620.TBL
|
|
12
|
-
# https://github.com/SheetJS/js-codepage/blob/master/codepages/895.TBL
|
|
12
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/620.TBL,
|
|
13
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/895.TBL and
|
|
14
|
+
# (Mac Central European) Python's mac_latin2 codec, which matches iconv's
|
|
15
|
+
# MACCENTRALEUROPE
|
|
13
16
|
|
|
14
17
|
# Mazovia (Polish) MS-DOS
|
|
15
18
|
CP620_TO_UNICODE = [
|
|
@@ -83,9 +86,46 @@ module DBF
|
|
|
83
86
|
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
|
84
87
|
].freeze
|
|
85
88
|
|
|
89
|
+
# Macintosh Central European (Ruby has the encoding but no converter)
|
|
90
|
+
MACCENTEURO_TO_UNICODE = [
|
|
91
|
+
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
|
|
92
|
+
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
93
|
+
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
|
|
94
|
+
0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
|
|
95
|
+
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
|
96
|
+
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
|
97
|
+
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
|
98
|
+
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
|
99
|
+
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
|
100
|
+
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
|
101
|
+
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
|
102
|
+
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
|
103
|
+
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
|
104
|
+
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
|
105
|
+
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
|
106
|
+
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
|
|
107
|
+
0x00C4, 0x0100, 0x0101, 0x00C9, 0x0104, 0x00D6, 0x00DC, 0x00E1,
|
|
108
|
+
0x0105, 0x010C, 0x00E4, 0x010D, 0x0106, 0x0107, 0x00E9, 0x0179,
|
|
109
|
+
0x017A, 0x010E, 0x00ED, 0x010F, 0x0112, 0x0113, 0x0116, 0x00F3,
|
|
110
|
+
0x0117, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x011A, 0x011B, 0x00FC,
|
|
111
|
+
0x2020, 0x00B0, 0x0118, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF,
|
|
112
|
+
0x00AE, 0x00A9, 0x2122, 0x0119, 0x00A8, 0x2260, 0x0123, 0x012E,
|
|
113
|
+
0x012F, 0x012A, 0x2264, 0x2265, 0x012B, 0x0136, 0x2202, 0x2211,
|
|
114
|
+
0x0142, 0x013B, 0x013C, 0x013D, 0x013E, 0x0139, 0x013A, 0x0145,
|
|
115
|
+
0x0146, 0x0143, 0x00AC, 0x221A, 0x0144, 0x0147, 0x2206, 0x00AB,
|
|
116
|
+
0x00BB, 0x2026, 0x00A0, 0x0148, 0x0150, 0x00D5, 0x0151, 0x014C,
|
|
117
|
+
0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA,
|
|
118
|
+
0x014D, 0x0154, 0x0155, 0x0158, 0x2039, 0x203A, 0x0159, 0x0156,
|
|
119
|
+
0x0157, 0x0160, 0x201A, 0x201E, 0x0161, 0x015A, 0x015B, 0x00C1,
|
|
120
|
+
0x0164, 0x0165, 0x00CD, 0x017D, 0x017E, 0x016A, 0x00D3, 0x00D4,
|
|
121
|
+
0x016B, 0x016E, 0x00DA, 0x016F, 0x0170, 0x0171, 0x0172, 0x0173,
|
|
122
|
+
0x00DD, 0x00FD, 0x0137, 0x017B, 0x0141, 0x017C, 0x0122, 0x02C7
|
|
123
|
+
].freeze
|
|
124
|
+
|
|
86
125
|
CUSTOM_TABLES = {
|
|
87
126
|
'cp620' => CP620_TO_UNICODE,
|
|
88
|
-
'cp895' => CP895_TO_UNICODE
|
|
127
|
+
'cp895' => CP895_TO_UNICODE,
|
|
128
|
+
'maccenteuro' => MACCENTEURO_TO_UNICODE
|
|
89
129
|
}.freeze
|
|
90
130
|
|
|
91
131
|
# Is the encoding a code page without a Ruby Encoding, decoded through
|
|
@@ -97,6 +137,21 @@ module DBF
|
|
|
97
137
|
encoding.is_a?(::String) && CUSTOM_TABLES.key?(encoding.downcase)
|
|
98
138
|
end
|
|
99
139
|
|
|
140
|
+
# Raise unless strings in the encoding can be transcoded to Unicode, so
|
|
141
|
+
# an unknown name or a dummy encoding such as UTF-7 fails up front with
|
|
142
|
+
# a DBF::Error rather than with ArgumentError or
|
|
143
|
+
# Encoding::ConverterNotFoundError on the first read.
|
|
144
|
+
#
|
|
145
|
+
# @param encoding [String, Encoding]
|
|
146
|
+
# @raise [DBF::UnsupportedEncodingError]
|
|
147
|
+
def self.check(encoding)
|
|
148
|
+
return if custom?(encoding)
|
|
149
|
+
|
|
150
|
+
(+'').force_encoding(encoding).encode(Encoding::UTF_8)
|
|
151
|
+
rescue ArgumentError, TypeError, Encoding::ConverterNotFoundError
|
|
152
|
+
raise DBF::UnsupportedEncodingError, "unsupported encoding: #{encoding}"
|
|
153
|
+
end
|
|
154
|
+
|
|
100
155
|
# Transcode a string from source_encoding to target_encoding
|
|
101
156
|
#
|
|
102
157
|
# @param string [String]
|
data/lib/dbf/errors.rb
CHANGED
|
@@ -6,7 +6,30 @@ module DBF
|
|
|
6
6
|
class Error < StandardError
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# A file cannot be opened. The message names only the file, not its
|
|
10
|
+
# directory, so an app that shows error messages to users does not reveal
|
|
11
|
+
# server paths; #path has the full path for logging.
|
|
12
|
+
class FileError < Error
|
|
13
|
+
# @return [String, nil] the full path of the file
|
|
14
|
+
attr_reader :path
|
|
15
|
+
|
|
16
|
+
def initialize(message = nil, path: nil)
|
|
17
|
+
super(message)
|
|
18
|
+
@path = path
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class FileNotFoundError < FileError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A file exists but cannot be opened, e.g. it is a directory or is not
|
|
26
|
+
# readable.
|
|
27
|
+
class FileAccessError < FileError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The encoding passed to Table.new is unknown, or Ruby cannot transcode
|
|
31
|
+
# it to Unicode.
|
|
32
|
+
class UnsupportedEncodingError < Error
|
|
10
33
|
end
|
|
11
34
|
|
|
12
35
|
class NoColumnsDefined < Error
|
data/lib/dbf/file_handler.rb
CHANGED
|
@@ -9,7 +9,7 @@ module DBF
|
|
|
9
9
|
when StringIO
|
|
10
10
|
data
|
|
11
11
|
when String
|
|
12
|
-
|
|
12
|
+
open_file(data)
|
|
13
13
|
else
|
|
14
14
|
raise ArgumentError, 'data must be a file path or an IO-like object responding to #read and #seek' unless data.respond_to?(:read) && data.respond_to?(:seek)
|
|
15
15
|
|
|
@@ -18,17 +18,41 @@ module DBF
|
|
|
18
18
|
data.binmode if data.respond_to?(:binmode)
|
|
19
19
|
data
|
|
20
20
|
end
|
|
21
|
-
rescue Errno::ENOENT
|
|
22
|
-
raise DBF::FileNotFoundError, "file not found: #{data}"
|
|
23
21
|
end
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
# Opens a file for binary reading, raising DBF errors instead of the
|
|
24
|
+
# system errors File.open raises, so callers need only rescue DBF::Error.
|
|
25
|
+
# A path containing a null byte cannot name a file, so it is not found.
|
|
26
|
+
# A directory opens without error but fails on the first read, so it is
|
|
27
|
+
# rejected here.
|
|
28
|
+
def open_file(path)
|
|
29
|
+
raise Errno::EISDIR if File.directory?(path)
|
|
30
|
+
|
|
31
|
+
File.open(path, 'rb')
|
|
32
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, ArgumentError
|
|
33
|
+
raise not_found(path)
|
|
34
|
+
rescue SystemCallError => e
|
|
35
|
+
reason = SystemCallError.new(nil, e.errno).message
|
|
36
|
+
raise DBF::FileAccessError.new("cannot open file: #{display_name(path)} (#{reason})", path:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def not_found(path)
|
|
40
|
+
DBF::FileNotFoundError.new("file not found: #{display_name(path)}", path:)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The file name without its directory, for error messages. A null byte,
|
|
44
|
+
# which File.basename rejects, is shown as "?".
|
|
45
|
+
def display_name(path)
|
|
46
|
+
File.basename(path.to_s.tr("\x00", '?'))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def open_memo(data, memo, memo_class, version, max_size: nil)
|
|
26
50
|
if memo
|
|
27
51
|
meth = memo.is_a?(String) ? :open : :new
|
|
28
|
-
memo_class.send(meth, memo, version)
|
|
52
|
+
memo_class.send(meth, memo, version, max_size:)
|
|
29
53
|
elsif (path = data_path(data))
|
|
30
|
-
found =
|
|
31
|
-
found && memo_class.open(found, version)
|
|
54
|
+
found = find_memo(path)
|
|
55
|
+
found && memo_class.open(found, version, max_size:)
|
|
32
56
|
end
|
|
33
57
|
end
|
|
34
58
|
|
|
@@ -38,10 +62,23 @@ module DBF
|
|
|
38
62
|
data.path if data.respond_to?(:path)
|
|
39
63
|
end
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
MEMO_EXTENSIONS = %w[fpt dbt].freeze
|
|
66
|
+
|
|
67
|
+
# Finds the memo file sitting next to the data file: same basename, with
|
|
68
|
+
# an .fpt or .dbt extension in any case. The directory is scanned rather
|
|
69
|
+
# than globbed so glob metacharacters in an uploaded filename cannot be
|
|
70
|
+
# expanded, and the basename must match exactly so data.dbf never picks
|
|
71
|
+
# up an unrelated memo such as data2.fpt.
|
|
72
|
+
def find_memo(path)
|
|
42
73
|
dirname = File.dirname(path)
|
|
43
74
|
basename = File.basename(path, '.*')
|
|
44
|
-
|
|
75
|
+
entries = Dir.children(dirname).sort.each_with_object({}) do |child, index|
|
|
76
|
+
index[child.downcase(:ascii)] ||= child
|
|
77
|
+
end
|
|
78
|
+
entry = MEMO_EXTENSIONS.filter_map { |ext| entries["#{basename}.#{ext}".downcase(:ascii)] }.first
|
|
79
|
+
entry && File.join(dirname, entry)
|
|
80
|
+
rescue SystemCallError
|
|
81
|
+
nil
|
|
45
82
|
end
|
|
46
83
|
end
|
|
47
84
|
end
|
data/lib/dbf/memo/base.rb
CHANGED
|
@@ -6,19 +6,25 @@ module DBF
|
|
|
6
6
|
BLOCK_HEADER_SIZE = 8
|
|
7
7
|
BLOCK_SIZE = 512
|
|
8
8
|
|
|
9
|
-
def self.open(filename, version)
|
|
10
|
-
new(
|
|
9
|
+
def self.open(filename, version, max_size: nil)
|
|
10
|
+
new(FileHandler.open_file(filename), version, max_size:)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# @param max_size [Integer, nil] bytes to read from each memo; longer
|
|
14
|
+
# memos are cut off. nil reads every memo in full.
|
|
15
|
+
def initialize(data, version, max_size: nil)
|
|
14
16
|
@data = data
|
|
15
17
|
@version = version
|
|
18
|
+
@max_size = max_size
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def get(start_block)
|
|
19
22
|
return nil unless start_block > 0
|
|
20
23
|
|
|
21
24
|
build_memo start_block
|
|
25
|
+
rescue RangeError
|
|
26
|
+
# A corrupt memo pointer too large to seek to has no memo
|
|
27
|
+
nil
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def close
|
|
@@ -31,7 +37,12 @@ module DBF
|
|
|
31
37
|
|
|
32
38
|
private
|
|
33
39
|
|
|
34
|
-
attr_reader :data
|
|
40
|
+
attr_reader :data, :max_size
|
|
41
|
+
|
|
42
|
+
# Bounds a memo length by max_size, when one is set
|
|
43
|
+
def limit(length) # :nodoc:
|
|
44
|
+
max_size && length > max_size ? max_size : length
|
|
45
|
+
end
|
|
35
46
|
|
|
36
47
|
def offset(start_block) # :nodoc:
|
|
37
48
|
start_block * block_size
|
data/lib/dbf/memo/dbase3.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module DBF
|
|
4
4
|
module Memo
|
|
5
5
|
class Dbase3 < Base
|
|
6
|
+
# Memo text up to, and whether it reached, the 0x1A terminator
|
|
7
|
+
TERMINATOR_PATTERN = /\A([^\x1A]*)(\x1A)?/n
|
|
8
|
+
|
|
6
9
|
def build_memo(start_block) # :nodoc:
|
|
7
10
|
data.seek offset(start_block)
|
|
8
11
|
memo_string = +''
|
|
@@ -12,11 +15,22 @@ module DBF
|
|
|
12
15
|
# than crashing on nil.gsub.
|
|
13
16
|
break if block.nil?
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
# The memo ends at its 0x1A terminator; bytes after it in the same
|
|
19
|
+
# block are leftovers, not memo text.
|
|
20
|
+
text, terminated = TERMINATOR_PATTERN.match(block).captures
|
|
21
|
+
text = text.delete("\000")
|
|
22
|
+
memo_string << text
|
|
23
|
+
break if terminated || text.size < BLOCK_SIZE || full?(memo_string)
|
|
18
24
|
end
|
|
19
|
-
memo_string
|
|
25
|
+
memo_string.byteslice(0, limit(memo_string.bytesize))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# A memo with no terminator runs to the end of the file, so stop
|
|
31
|
+
# reading blocks once max_size bytes have been read.
|
|
32
|
+
def full?(memo_string) # :nodoc:
|
|
33
|
+
max_size && memo_string.bytesize >= max_size
|
|
20
34
|
end
|
|
21
35
|
end
|
|
22
36
|
end
|
data/lib/dbf/memo/dbase4.rb
CHANGED
|
@@ -11,7 +11,7 @@ module DBF
|
|
|
11
11
|
header = data.read(BLOCK_HEADER_SIZE)
|
|
12
12
|
return nil unless header && header.bytesize == BLOCK_HEADER_SIZE
|
|
13
13
|
|
|
14
|
-
length = header.unpack1('x4L')
|
|
14
|
+
length = limit(header.unpack1('x4L'))
|
|
15
15
|
|
|
16
16
|
# Bound the read by the bytes remaining so a crafted 32-bit length
|
|
17
17
|
# field cannot force a ~4 GiB allocation from a small memo file.
|
data/lib/dbf/memo/foxpro.rb
CHANGED
|
@@ -14,7 +14,7 @@ module DBF
|
|
|
14
14
|
memo_type, memo_size, memo_string = block.unpack('NNa*')
|
|
15
15
|
return nil unless memo_type == 1 && memo_size.to_i.positive?
|
|
16
16
|
|
|
17
|
-
read_memo_content(memo_string, memo_size)
|
|
17
|
+
read_memo_content(memo_string, limit(memo_size))
|
|
18
18
|
rescue IOError, SystemCallError, RangeError
|
|
19
19
|
nil
|
|
20
20
|
end
|
data/lib/dbf/record.rb
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
module DBF
|
|
4
4
|
# An instance of DBF::Record represents a row in the DBF file
|
|
5
5
|
class Record
|
|
6
|
+
# Column names come from the file, so they are never exposed as these
|
|
7
|
+
# methods: Ruby (and Rails) call them implicitly to convert or render an
|
|
8
|
+
# object, which would let a crafted file control how a record behaves.
|
|
9
|
+
# Such columns remain readable through #[] and #attributes.
|
|
10
|
+
RESERVED_METHODS = %w[
|
|
11
|
+
to_str to_ary to_hash to_proc to_int to_io to_path to_regexp to_open
|
|
12
|
+
to_model to_partial_path to_param to_key
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
6
15
|
# Initialize a new DBF::Record
|
|
7
16
|
#
|
|
8
17
|
# @param data [String, StringIO] data
|
|
@@ -112,7 +121,7 @@ module DBF
|
|
|
112
121
|
|
|
113
122
|
def method_missing(method, *args) # :nodoc:
|
|
114
123
|
key = method.to_s
|
|
115
|
-
if (index =
|
|
124
|
+
if (index = column_method_index(key))
|
|
116
125
|
if @context.column_offsets && !@to_a
|
|
117
126
|
column_value(index)
|
|
118
127
|
else
|
|
@@ -124,7 +133,13 @@ module DBF
|
|
|
124
133
|
end
|
|
125
134
|
|
|
126
135
|
def respond_to_missing?(method, *) # :nodoc:
|
|
127
|
-
|
|
136
|
+
!column_method_index(method.to_s).nil? || super
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def column_method_index(key) # :nodoc:
|
|
140
|
+
return nil if RESERVED_METHODS.include?(key)
|
|
141
|
+
|
|
142
|
+
underscored_column_names.index(key)
|
|
128
143
|
end
|
|
129
144
|
|
|
130
145
|
def underscored_column_names # :nodoc:
|
data/lib/dbf/table.rb
CHANGED
|
@@ -41,6 +41,9 @@ module DBF
|
|
|
41
41
|
# table = DBF::Table.new 'data.dbf', nil, 'cp437'
|
|
42
42
|
# table = DBF::Table.new 'data.dbf', 'memo.dbt', Encoding::US_ASCII
|
|
43
43
|
#
|
|
44
|
+
# # reading at most the first 64 KB of each memo
|
|
45
|
+
# table = DBF::Table.new 'data.dbf', max_memo_size: 65_536
|
|
46
|
+
#
|
|
44
47
|
# Opens a table like .new, but when given a block, yields the table,
|
|
45
48
|
# closes it when the block returns, and returns the block's value —
|
|
46
49
|
# the same contract as File.open.
|
|
@@ -50,8 +53,8 @@ module DBF
|
|
|
50
53
|
# end
|
|
51
54
|
#
|
|
52
55
|
# Takes the same arguments as .new. Without a block, equivalent to .new.
|
|
53
|
-
def self.open(data, memo = nil, encoding = nil, name: nil)
|
|
54
|
-
table = new(data, memo, encoding, name:
|
|
56
|
+
def self.open(data, memo = nil, encoding = nil, name: nil, max_memo_size: nil)
|
|
57
|
+
table = new(data, memo, encoding, name:, max_memo_size:)
|
|
55
58
|
return table unless block_given?
|
|
56
59
|
|
|
57
60
|
begin
|
|
@@ -64,11 +67,15 @@ module DBF
|
|
|
64
67
|
# @param data [String, StringIO, IO] data Path to the dbf file or an IO-like object
|
|
65
68
|
# @param memo [optional String, StringIO, IO] memo Path to the memo file or an IO-like object
|
|
66
69
|
# @param encoding [optional String, Encoding] encoding Name of the encoding or an Encoding object
|
|
67
|
-
|
|
70
|
+
# @param max_memo_size [optional Integer] Bytes to read from each memo;
|
|
71
|
+
# longer memos are cut off. Every memo is read in full by default.
|
|
72
|
+
def initialize(data, memo = nil, encoding = nil, name: nil, max_memo_size: nil)
|
|
73
|
+
Encoder.check(encoding) if encoding
|
|
74
|
+
validate_max_memo_size(max_memo_size)
|
|
68
75
|
@data = FileHandler.open_data(data)
|
|
69
76
|
@user_encoding = encoding
|
|
70
77
|
@encoding = determine_encoding
|
|
71
|
-
@memo =
|
|
78
|
+
@memo = open_memo(data, memo, max_memo_size)
|
|
72
79
|
@name = name
|
|
73
80
|
yield self if block_given?
|
|
74
81
|
end
|
|
@@ -148,6 +155,9 @@ module DBF
|
|
|
148
155
|
# @return [DBF::Record, NilClass]
|
|
149
156
|
def record(index)
|
|
150
157
|
raise DBF::NoColumnsDefined, 'The DBF file has no columns defined' if columns.empty?
|
|
158
|
+
# A negative index would read header bytes as a record (or raise
|
|
159
|
+
# Errno::EINVAL); treat it, like any index past the end, as absent.
|
|
160
|
+
return nil unless index >= 0 && index < record_count
|
|
151
161
|
|
|
152
162
|
seek_to_record(index)
|
|
153
163
|
return nil if deleted_record?
|
|
@@ -237,8 +247,23 @@ module DBF
|
|
|
237
247
|
@user_encoding || header.encoding || Encoding.default_external
|
|
238
248
|
end
|
|
239
249
|
|
|
250
|
+
# Close a data file opened from a path if its memo cannot be opened, so
|
|
251
|
+
# the failed Table does not leak a file handle.
|
|
252
|
+
def open_memo(data, memo, max_size) # :nodoc:
|
|
253
|
+
FileHandler.open_memo(data, memo, version_config.memo_class, version, max_size:)
|
|
254
|
+
rescue DBF::Error
|
|
255
|
+
@data.close if data.is_a?(String)
|
|
256
|
+
raise
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def validate_max_memo_size(max_memo_size) # :nodoc:
|
|
260
|
+
return if max_memo_size.nil? || (max_memo_size.is_a?(Integer) && max_memo_size.positive?)
|
|
261
|
+
|
|
262
|
+
raise ArgumentError, 'max_memo_size must be a positive Integer or nil'
|
|
263
|
+
end
|
|
264
|
+
|
|
240
265
|
def build_columns # :nodoc:
|
|
241
|
-
ColumnBuilder.new(self, @data, version_config).build
|
|
266
|
+
ColumnBuilder.new(self, @data, version_config, header_length:, record_length:).build
|
|
242
267
|
end
|
|
243
268
|
|
|
244
269
|
def deleted_record? # :nodoc:
|
data/lib/dbf/version.rb
CHANGED
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keith Morrison
|
|
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '0'
|
|
86
86
|
requirements: []
|
|
87
|
-
rubygems_version:
|
|
87
|
+
rubygems_version: 4.0.20
|
|
88
88
|
specification_version: 4
|
|
89
89
|
summary: Read xBase files
|
|
90
90
|
test_files: []
|