dbf 5.4.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.
data/lib/dbf/table.rb CHANGED
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DBF
4
- class FileNotFoundError < StandardError
5
- end
6
-
7
- class NoColumnsDefined < StandardError
8
- end
9
-
10
4
  # Leading bytes that make a spreadsheet treat a CSV cell as a formula:
11
5
  # "=", "+", "-", "@", tab, and carriage return.
12
6
  CSV_FORMULA_TRIGGERS = [0x3D, 0x2B, 0x2D, 0x40, 0x09, 0x0D].freeze
@@ -37,6 +31,9 @@ module DBF
37
31
  # # working with a dbf in memory
38
32
  # table = DBF::Table.new StringIO.new(dbf_data)
39
33
  #
34
+ # # working with an open IO object
35
+ # table = DBF::Table.new File.open('data.dbf', 'rb')
36
+ #
40
37
  # # working with a dbf and memo in memory
41
38
  # table = DBF::Table.new StringIO.new(dbf_data), StringIO.new(memo_data)
42
39
  #
@@ -44,14 +41,41 @@ module DBF
44
41
  # table = DBF::Table.new 'data.dbf', nil, 'cp437'
45
42
  # table = DBF::Table.new 'data.dbf', 'memo.dbt', Encoding::US_ASCII
46
43
  #
47
- # @param data [String, StringIO] data Path to the dbf file or a StringIO object
48
- # @param memo [optional String, StringIO] memo Path to the memo file or a StringIO object
44
+ # # reading at most the first 64 KB of each memo
45
+ # table = DBF::Table.new 'data.dbf', max_memo_size: 65_536
46
+ #
47
+ # Opens a table like .new, but when given a block, yields the table,
48
+ # closes it when the block returns, and returns the block's value —
49
+ # the same contract as File.open.
50
+ #
51
+ # DBF::Table.open('data.dbf') do |table|
52
+ # table.each { |record| ... }
53
+ # end
54
+ #
55
+ # Takes the same arguments as .new. Without a block, equivalent to .new.
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:)
58
+ return table unless block_given?
59
+
60
+ begin
61
+ yield table
62
+ ensure
63
+ table.close
64
+ end
65
+ end
66
+
67
+ # @param data [String, StringIO, IO] data Path to the dbf file or an IO-like object
68
+ # @param memo [optional String, StringIO, IO] memo Path to the memo file or an IO-like object
49
69
  # @param encoding [optional String, Encoding] encoding Name of the encoding or an Encoding object
50
- def initialize(data, memo = nil, encoding = nil, name: nil)
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)
51
75
  @data = FileHandler.open_data(data)
52
76
  @user_encoding = encoding
53
77
  @encoding = determine_encoding
54
- @memo = FileHandler.open_memo(data, memo, version_config.memo_class, version)
78
+ @memo = open_memo(data, memo, max_memo_size)
55
79
  @name = name
56
80
  yield self if block_given?
57
81
  end
@@ -110,7 +134,7 @@ module DBF
110
134
 
111
135
  # @return [String]
112
136
  def filename
113
- File.basename(@data.path) if @data.is_a?(File)
137
+ File.basename(@data.path) if @data.respond_to?(:path)
114
138
  end
115
139
 
116
140
  # @return [TrueClass, FalseClass]
@@ -131,6 +155,9 @@ module DBF
131
155
  # @return [DBF::Record, NilClass]
132
156
  def record(index)
133
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
134
161
 
135
162
  seek_to_record(index)
136
163
  return nil if deleted_record?
@@ -150,14 +177,11 @@ module DBF
150
177
  #
151
178
  # @param [optional String, IO] path_or_io String path, IO-like object, or nil for STDOUT
152
179
  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
180
+ if path_or_io.is_a?(String)
181
+ File.open(path_or_io, 'w') { |file| write_csv(file) }
182
+ else
183
+ write_csv(path_or_io || $stdout)
157
184
  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) } }
161
185
  end
162
186
 
163
187
  # Human readable version description
@@ -184,6 +208,12 @@ module DBF
184
208
 
185
209
  private
186
210
 
211
+ def write_csv(io) # :nodoc:
212
+ csv = CSV.new(io, force_quotes: true)
213
+ csv << column_names.map { |name| csv_safe_value(name) }
214
+ each { |record| csv << record.to_a.map { |value| csv_safe_value(value) } }
215
+ end
216
+
187
217
  # Neutralizes spreadsheet formula injection (CWE-1236) on CSV export by
188
218
  # prefixing a single quote to string cells that begin with a formula
189
219
  # trigger character. Non-string values (numbers, dates, booleans) are
@@ -217,8 +247,23 @@ module DBF
217
247
  @user_encoding || header.encoding || Encoding.default_external
218
248
  end
219
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
+
220
265
  def build_columns # :nodoc:
221
- ColumnBuilder.new(self, @data, version_config).build
266
+ ColumnBuilder.new(self, @data, version_config, header_length:, record_length:).build
222
267
  end
223
268
 
224
269
  def deleted_record? # :nodoc:
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.4.0'
4
+ VERSION = '5.6.0'
5
5
  end
data/lib/dbf.rb CHANGED
@@ -7,6 +7,7 @@ require 'json'
7
7
  require 'time'
8
8
 
9
9
  require 'dbf/version'
10
+ require 'dbf/errors'
10
11
  require 'dbf/schema'
11
12
  require 'dbf/find'
12
13
  require 'dbf/record'
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.0
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Morrison
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: csv
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: '3.3'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ">="
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: '3.3'
26
26
  description: A small fast library for reading dBase, xBase, Clipper and FoxPro database
27
27
  files.
28
28
  email: keithm@infused.org
@@ -44,6 +44,7 @@ files:
44
44
  - lib/dbf/database/foxpro.rb
45
45
  - lib/dbf/encoder.rb
46
46
  - lib/dbf/encodings.rb
47
+ - lib/dbf/errors.rb
47
48
  - lib/dbf/file_handler.rb
48
49
  - lib/dbf/find.rb
49
50
  - lib/dbf/header.rb
@@ -63,8 +64,12 @@ licenses:
63
64
  - MIT
64
65
  metadata:
65
66
  rubygems_mfa_required: 'true'
67
+ homepage_uri: https://github.com/infused/dbf
66
68
  source_code_uri: https://github.com/infused/dbf
67
69
  changelog_uri: https://github.com/infused/dbf/blob/main/CHANGELOG.md
70
+ bug_tracker_uri: https://github.com/infused/dbf/issues
71
+ documentation_uri: https://dbf.infused.org
72
+ funding_uri: https://github.com/sponsors/infused
68
73
  rdoc_options: []
69
74
  require_paths:
70
75
  - lib
@@ -79,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
84
  - !ruby/object:Gem::Version
80
85
  version: '0'
81
86
  requirements: []
82
- rubygems_version: 4.0.17
87
+ rubygems_version: 4.0.20
83
88
  specification_version: 4
84
89
  summary: Read xBase files
85
90
  test_files: []