dbf-4.3.2/ 0000755 0000041 0000041 00000000000 14572252217 012312 5 ustar www-data www-data dbf-4.3.2/bin/ 0000755 0000041 0000041 00000000000 14572252217 013062 5 ustar www-data www-data dbf-4.3.2/bin/dbf 0000755 0000041 0000041 00000002707 14572252217 013551 0 ustar www-data www-data #!/usr/bin/env ruby
Signal.trap('PIPE', 'SYSTEM_DEFAULT')
require 'dbf'
require 'dbf/version'
require 'optparse'
params = ARGV.getopts('h', 's', 'a', 'c', 'r', 'v')
if params['v']
puts "dbf version: #{DBF::VERSION}"
elsif params['h']
puts "usage: #{File.basename(__FILE__)} [-h|-s|-a|-c|-r] filename"
puts ' -h = print this message'
puts ' -v = print the DBF gem version'
puts ' -s = print summary information'
puts ' -a = create an ActiveRecord::Schema'
puts ' -r = create a Sequel migration'
puts ' -c = export as CSV'
else
filename = ARGV.shift
abort 'You must supply a filename on the command line' unless filename
# create an ActiveRecord::Schema
if params['a']
table = DBF::Table.new filename
puts table.schema(:activerecord)
end
# create an Sequel::Migration
if params['r']
table = DBF::Table.new filename
puts table.schema(:sequel)
end
if params['s']
table = DBF::Table.new filename
puts
puts "Database: #{filename}"
puts "Type: (#{table.version}) #{table.version_description}"
puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}"
puts "Records: #{table.record_count}"
puts "\nFields:"
puts 'Name Type Length Decimal'
puts '-' * 78
table.columns.each do |f|
puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal)
end
end
if params['c']
table = DBF::Table.new filename
table.to_csv
end
end
dbf-4.3.2/dbf.gemspec 0000644 0000041 0000041 00000001653 14572252217 014417 0 ustar www-data www-data lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
require 'dbf/version'
Gem::Specification.new do |s|
s.name = 'dbf'
s.version = DBF::VERSION
s.authors = ['Keith Morrison']
s.email = 'keithm@infused.org'
s.homepage = 'http://github.com/infused/dbf'
s.summary = 'Read xBase files'
s.description = 'A small fast library for reading dBase, xBase, Clipper and FoxPro database files.'
s.license = 'MIT'
s.bindir = 'bin'
s.executables = ['dbf']
s.rdoc_options = ['--charset=UTF-8']
s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE']
s.files = Dir['README.md', 'CHANGELOG.md', 'LICENSE', '{bin,lib,spec}/**/*', 'dbf.gemspec']
s.require_paths = ['lib']
s.required_rubygems_version = Gem::Requirement.new('>= 1.3.0')
s.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
s.metadata['rubygems_mfa_required'] = 'true'
s.add_runtime_dependency 'csv'
end
dbf-4.3.2/lib/ 0000755 0000041 0000041 00000000000 14572252217 013060 5 ustar www-data www-data dbf-4.3.2/lib/dbf/ 0000755 0000041 0000041 00000000000 14572252217 013613 5 ustar www-data www-data dbf-4.3.2/lib/dbf/database/ 0000755 0000041 0000041 00000000000 14572252217 015357 5 ustar www-data www-data dbf-4.3.2/lib/dbf/database/foxpro.rb 0000644 0000041 0000041 00000007455 14572252217 017234 0 ustar www-data www-data module DBF
# DBF::Database::Foxpro is the primary interface to a Visual Foxpro database
# container (.dbc file). When using this database container, long fieldnames
# are supported, and you can reference tables directly instead of
# instantiating Table objects yourself.
# Table references are created based on the filename, but it this class
# tries to correct the table filenames because they could be wrong for
# case sensitive filesystems, e.g. when a foxpro database is uploaded to
# a linux server.
module Database
class Foxpro
# Opens a DBF::Database::Foxpro
# Examples:
# # working with a database stored on the filesystem
# db = DBF::Database::Foxpro.new 'path_to_db/database.dbc'
#
# # Calling a table
# contacts = db.contacts.record(0)
#
# @param path [String]
def initialize(path)
@path = path
@dirname = File.dirname(@path)
@db = DBF::Table.new(@path)
@tables = extract_dbc_data
rescue Errno::ENOENT
raise DBF::FileNotFoundError, "file not found: #{data}"
end
def table_names
@tables.keys
end
# Returns table with given name
#
# @param name [String]
# @return [DBF::Table]
def table(name)
Table.new table_path(name) do |table|
table.long_names = @tables[name]
end
end
# Searches the database directory for the table's dbf file
# and returns the absolute path. Ensures case-insensitivity
# on any platform.
# @param name [String]
# @return [String]
def table_path(name)
glob = File.join(@dirname, "#{name}.dbf")
path = Dir.glob(glob, File::FNM_CASEFOLD).first
raise DBF::FileNotFoundError, "related table not found: #{name}" unless path && File.exist?(path)
path
end
def method_missing(method, *args) # :nodoc:
table_names.index(method.to_s) ? table(method.to_s) : super
end
def respond_to_missing?(method, *)
table_names.index(method.to_s) || super
end
private
# This method extracts the data from the database container. This is
# just an ordinary table with a treelike structure. Field definitions
# are in the same order as in the linked tables but only the long name
# is provided.
def extract_dbc_data # :nodoc:
data = {}
@db.each do |record|
next unless record
case record.objecttype
when 'Table'
# This is a related table
process_table record, data
when 'Field'
# This is a related field. The parentid points to the table object.
# Create using the parentid if the parentid is still unknown.
process_field record, data
end
end
Hash[
data.values.map { |v| [v[:name], v[:fields]] }
]
end
def process_table(record, data)
id = record.objectid
name = record.objectname
data[id] = table_field_hash(name)
end
def process_field(record, data)
id = record.parentid
name = 'UNKNOWN'
field = record.objectname
data[id] ||= table_field_hash(name)
data[id][:fields] << field
end
def table_field_hash(name)
{name: name, fields: []}
end
end
class Table < DBF::Table
attr_accessor :long_names
def build_columns # :nodoc:
columns = super
# modify the column definitions to use the long names as the
# columnname property is readonly, recreate the column definitions
columns.map do |column|
long_name = long_names[columns.index(column)]
Column.new(self, long_name, column.type, column.length, column.decimal)
end
end
end
end
end
dbf-4.3.2/lib/dbf/header.rb 0000644 0000041 0000041 00000001105 14572252217 015365 0 ustar www-data www-data module DBF
class Header
attr_reader :version, :record_count, :header_length, :record_length, :encoding_key, :encoding
def initialize(data)
@data = data
unpack_header
end
def unpack_header
@version = @data.unpack1('H2')
case @version
when '02'
@record_count, @record_length = @data.unpack('x v x3 v')
@header_length = 521
else
@record_count, @header_length, @record_length, @encoding_key = @data.unpack('x x3 V v2 x17 H2')
@encoding = DBF::ENCODINGS[@encoding_key]
end
end
end
end
dbf-4.3.2/lib/dbf/schema.rb 0000644 0000041 0000041 00000006622 14572252217 015406 0 ustar www-data www-data module DBF
# The Schema module is mixin for the Table class
module Schema
FORMATS = [:activerecord, :json, :sequel].freeze
OTHER_DATA_TYPES = {
'Y' => ':decimal, :precision => 15, :scale => 4',
'D' => ':date',
'T' => ':datetime',
'L' => ':boolean',
'M' => ':text',
'B' => ':binary'
}.freeze
# Generate an ActiveRecord::Schema
#
# xBase data types are converted to generic types as follows:
# - Number columns with no decimals are converted to :integer
# - Number columns with decimals are converted to :float
# - Date columns are converted to :datetime
# - Logical columns are converted to :boolean
# - Memo columns are converted to :text
# - Character columns are converted to :string and the :limit option is set
# to the length of the character column
#
# Example:
# create_table "mydata" do |t|
# t.column :name, :string, :limit => 30
# t.column :last_update, :datetime
# t.column :is_active, :boolean
# t.column :age, :integer
# t.column :notes, :text
# end
#
# @param format [Symbol] format Valid options are :activerecord and :json
# @param table_only [Boolean]
# @return [String]
def schema(format = :activerecord, table_only: false)
schema_method_name = schema_name(format)
send(schema_method_name, table_only: table_only)
rescue NameError
raise ArgumentError, ":#{format} is not a valid schema. Valid schemas are: #{FORMATS.join(', ')}."
end
def schema_name(format) # :nodoc:
"#{format}_schema"
end
def activerecord_schema(*) # :nodoc:
s = "ActiveRecord::Schema.define do\n"
s << " create_table \"#{name}\" do |t|\n"
columns.each do |column|
s << " t.column #{activerecord_schema_definition(column)}"
end
s << " end\nend"
s
end
def sequel_schema(table_only: false) # :nodoc:
s = ''
s << "Sequel.migration do\n" unless table_only
s << " change do\n " unless table_only
s << " create_table(:#{name}) do\n"
columns.each do |column|
s << " column #{sequel_schema_definition(column)}"
end
s << " end\n"
s << " end\n" unless table_only
s << "end\n" unless table_only
s
end
def json_schema(*) # :nodoc:
columns.map(&:to_hash).to_json
end
# ActiveRecord schema definition
#
# @param column [DBF::Column]
# @return [String]
def activerecord_schema_definition(column)
"\"#{column.underscored_name}\", #{schema_data_type(column, :activerecord)}\n"
end
# Sequel schema definition
#
# @param column [DBF::Column]
# @return [String]
def sequel_schema_definition(column)
":#{column.underscored_name}, #{schema_data_type(column, :sequel)}\n"
end
def schema_data_type(column, format = :activerecord) # :nodoc:
case column.type
when 'N', 'F', 'I'
number_data_type(column)
when 'Y', 'D', 'T', 'L', 'M', 'B'
OTHER_DATA_TYPES[column.type]
else
string_data_format(format, column)
end
end
def number_data_type(column)
column.decimal > 0 ? ':float' : ':integer'
end
def string_data_format(format, column)
if format == :sequel
":varchar, :size => #{column.length}"
else
":string, :limit => #{column.length}"
end
end
end
end
dbf-4.3.2/lib/dbf/column.rb 0000644 0000041 0000041 00000005536 14572252217 015446 0 ustar www-data www-data module DBF
class Column
extend Forwardable
class LengthError < StandardError
end
class NameError < StandardError
end
attr_reader :table, :name, :type, :length, :decimal
def_delegator :type_cast_class, :type_cast
# rubocop:disable Style/MutableConstant
TYPE_CAST_CLASS = {
N: ColumnType::Number,
I: ColumnType::SignedLong,
F: ColumnType::Float,
Y: ColumnType::Currency,
D: ColumnType::Date,
T: ColumnType::DateTime,
L: ColumnType::Boolean,
M: ColumnType::Memo,
B: ColumnType::Double,
G: ColumnType::General,
'+'.to_sym => ColumnType::SignedLong2
}
# rubocop:enable Style/MutableConstant
TYPE_CAST_CLASS.default = ColumnType::String
TYPE_CAST_CLASS.freeze
# Initialize a new DBF::Column
#
# @param table [String]
# @param name [String]
# @param type [String]
# @param length [Integer]
# @param decimal [Integer]
def initialize(table, name, type, length, decimal)
@table = table
@name = clean(name)
@type = type
@length = length
@decimal = decimal
@version = table.version
@encoding = table.encoding
validate_length
validate_name
end
# Returns true if the column is a memo
#
# @return [Boolean]
def memo?
@memo ||= type == 'M'
end
# Returns a Hash with :name, :type, :length, and :decimal keys
#
# @return [Hash]
def to_hash
{name: name, type: type, length: length, decimal: decimal}
end
# Underscored name
#
# This is the column name converted to underscore format.
# For example, MyColumn will be returned as my_column.
#
# @return [String]
def underscored_name
@underscored_name ||= name.gsub(/([a-z\d])([A-Z])/, '\1_\2').tr('-', '_').downcase
end
private
def clean(value) # :nodoc:
value.strip.partition("\x00").first.gsub(/[^\x20-\x7E]/, '')
end
def encode(value, strip_output: false) # :nodoc:
return value unless value.respond_to?(:encoding)
output = @encoding ? encode_string(value) : value
strip_output ? output.strip : output
end
def encoding_args # :nodoc:
@encoding_args ||= [
Encoding.default_external,
{undef: :replace, invalid: :replace}
]
end
def encode_string(string) # :nodoc:
string.force_encoding(@encoding).encode(*encoding_args)
end
def type_cast_class # :nodoc:
@type_cast_class ||= begin
klass = @length == 0 ? ColumnType::Nil : TYPE_CAST_CLASS[type.to_sym]
klass.new(@decimal, @encoding)
end
end
def validate_length # :nodoc:
raise LengthError, 'field length must be 0 or greater' if length < 0
end
def validate_name # :nodoc:
raise NameError, 'column name cannot be empty' if @name.empty?
end
end
end
dbf-4.3.2/lib/dbf/encodings.rb 0000644 0000041 0000041 00000005125 14572252217 016114 0 ustar www-data www-data module DBF
ENCODINGS = {
'01' => 'cp437', # U.S. MS-DOS
'02' => 'cp850', # International MS-DOS
'03' => 'cp1252', # Windows ANSI
'08' => 'cp865', # Danish OEM
'09' => 'cp437', # Dutch OEM
'0a' => 'cp850', # Dutch OEM*
'0b' => 'cp437', # Finnish OEM
'0d' => 'cp437', # French OEM
'0e' => 'cp850', # French OEM*
'0f' => 'cp437', # German OEM
'10' => 'cp850', # German OEM*
'11' => 'cp437', # Italian OEM
'12' => 'cp850', # Italian OEM*
'13' => 'cp932', # Japanese Shift-JIS
'14' => 'cp850', # Spanish OEM*
'15' => 'cp437', # Swedish OEM
'16' => 'cp850', # Swedish OEM*
'17' => 'cp865', # Norwegian OEM
'18' => 'cp437', # Spanish OEM
'19' => 'cp437', # English OEM (Britain)
'1a' => 'cp850', # English OEM (Britain)*
'1b' => 'cp437', # English OEM (U.S.)
'1c' => 'cp863', # French OEM (Canada)
'1d' => 'cp850', # French OEM*
'1f' => 'cp852', # Czech OEM
'22' => 'cp852', # Hungarian OEM
'23' => 'cp852', # Polish OEM
'24' => 'cp860', # Portuguese OEM
'25' => 'cp850', # Portuguese OEM*
'26' => 'cp866', # Russian OEM
'37' => 'cp850', # English OEM (U.S.)*
'40' => 'cp852', # Romanian OEM
'4d' => 'cp936', # Chinese GBK (PRC)
'4e' => 'cp949', # Korean (ANSI/OEM)
'4f' => 'cp950', # Chinese Big5 (Taiwan)
'50' => 'cp874', # Thai (ANSI/OEM)
'57' => 'cp1252', # ANSI
'58' => 'cp1252', # Western European ANSI
'59' => 'cp1252', # Spanish ANSI
'64' => 'cp852', # Eastern European MS-DOS
'65' => 'cp866', # Russian MS-DOS
'66' => 'cp865', # Nordic MS-DOS
'67' => 'cp861', # Icelandic MS-DOS
'6a' => 'cp737', # Greek MS-DOS (437G)
'6b' => 'cp857', # Turkish MS-DOS
'6c' => 'cp863', # French-Canadian MS-DOS
'78' => 'cp950', # Taiwan Big 5
'79' => 'cp949', # Hangul (Wansung)
'7a' => 'cp936', # PRC GBK
'7b' => 'cp932', # Japanese Shift-JIS
'7c' => 'cp874', # Thai Windows/MS-DOS
'86' => 'cp737', # Greek OEM
'87' => 'cp852', # Slovenian OEM
'88' => 'cp857', # Turkish OEM
'c8' => 'cp1250', # Eastern European Windows
'c9' => 'cp1251', # Russian Windows
'ca' => 'cp1254', # Turkish Windows
'cb' => 'cp1253', # Greek Windows
'cc' => 'cp1257' # Baltic Windows
}.freeze
end
dbf-4.3.2/lib/dbf/table.rb 0000644 0000041 0000041 00000021215 14572252217 015230 0 ustar www-data www-data module DBF
class FileNotFoundError < StandardError
end
class NoColumnsDefined < StandardError
end
# DBF::Table is the primary interface to a single DBF file and provides
# methods for enumerating and searching the records.
class Table
extend Forwardable
include Enumerable
include ::DBF::Schema
DBASE2_HEADER_SIZE = 8
DBASE3_HEADER_SIZE = 32
DBASE7_HEADER_SIZE = 68
VERSIONS = {
'02' => 'FoxBase',
'03' => 'dBase III without memo file',
'04' => 'dBase IV without memo file',
'05' => 'dBase V without memo file',
'07' => 'Visual Objects 1.x',
'30' => 'Visual FoxPro',
'32' => 'Visual FoxPro with field type Varchar or Varbinary',
'31' => 'Visual FoxPro with AutoIncrement field',
'43' => 'dBASE IV SQL table files, no memo',
'63' => 'dBASE IV SQL system files, no memo',
'7b' => 'dBase IV with memo file',
'83' => 'dBase III with memo file',
'87' => 'Visual Objects 1.x with memo file',
'8b' => 'dBase IV with memo file',
'8c' => 'dBase 7',
'8e' => 'dBase IV with SQL table',
'cb' => 'dBASE IV SQL table files, with memo',
'f5' => 'FoxPro with memo file',
'fb' => 'FoxPro without memo file'
}.freeze
FOXPRO_VERSIONS = {
'30' => 'Visual FoxPro',
'31' => 'Visual FoxPro with AutoIncrement field',
'f5' => 'FoxPro with memo file',
'fb' => 'FoxPro without memo file'
}.freeze
attr_accessor :encoding
attr_writer :name
def_delegator :header, :header_length
def_delegator :header, :record_count
def_delegator :header, :record_length
def_delegator :header, :version
# Opens a DBF::Table
# Examples:
# # working with a file stored on the filesystem
# table = DBF::Table.new 'data.dbf'
#
# # working with a misnamed memo file
# table = DBF::Table.new 'data.dbf', 'memo.dbt'
#
# # working with a dbf in memory
# table = DBF::Table.new StringIO.new(dbf_data)
#
# # working with a dbf and memo in memory
# table = DBF::Table.new StringIO.new(dbf_data), StringIO.new(memo_data)
#
# # working with a dbf overriding specified in the dbf encoding
# table = DBF::Table.new 'data.dbf', nil, 'cp437'
# table = DBF::Table.new 'data.dbf', 'memo.dbt', Encoding::US_ASCII
#
# @param data [String, StringIO] data Path to the dbf file or a StringIO object
# @param memo [optional String, StringIO] memo Path to the memo file or a StringIO object
# @param encoding [optional String, Encoding] encoding Name of the encoding or an Encoding object
def initialize(data, memo = nil, encoding = nil)
@data = open_data(data)
@encoding = encoding || header.encoding
@memo = open_memo(data, memo)
yield self if block_given?
end
# Closes the table and memo file
#
# @return [TrueClass, FalseClass]
def close
@data.close
@memo&.close
end
# @return [TrueClass, FalseClass]
def closed?
if @memo
@data.closed? && @memo.closed?
else
@data.closed?
end
end
# Column names
#
# @return [String]
def column_names
@column_names ||= columns.map(&:name)
end
# All columns
#
# @return [Array]
def columns
@columns ||= build_columns
end
# Calls block once for each record in the table. The record may be nil
# if the record has been marked as deleted.
#
# @yield [nil, DBF::Record]
def each
record_count.times { |i| yield record(i) }
end
# @return [String]
def filename
return unless @data.respond_to?(:path)
File.basename(@data.path)
end
# Find records using a simple ActiveRecord-like syntax.
#
# Examples:
# table = DBF::Table.new 'mydata.dbf'
#
# # Find record number 5
# table.find(5)
#
# # Find all records for Keith Morrison
# table.find :all, first_name: "Keith", last_name: "Morrison"
#
# # Find first record
# table.find :first, first_name: "Keith"
#
# The command may be a record index, :all, or :first.
# options is optional and, if specified, should be a hash where the
# keys correspond to column names in the database. The values will be
# matched exactly with the value in the database. If you specify more
# than one key, all values must match in order for the record to be
# returned. The equivalent SQL would be "WHERE key1 = 'value1'
# AND key2 = 'value2'".
#
# @param command [Integer, Symbol] command
# @param options [optional, Hash] options Hash of search parameters
# @yield [optional, DBF::Record, NilClass]
def find(command, options = {}, &block)
case command
when Integer
record(command)
when Array
command.map { |i| record(i) }
when :all
find_all(options, &block)
when :first
find_first(options)
end
end
# @return [TrueClass, FalseClass]
def has_memo_file?
!!@memo
end
# @return [String]
def name
@name ||= filename && File.basename(filename, '.*')
end
# Retrieve a record by index number.
# The record will be nil if it has been deleted, but not yet pruned from
# the database.
#
# @param [Integer] index
# @return [DBF::Record, NilClass]
def record(index)
raise DBF::NoColumnsDefined, 'The DBF file has no columns defined' if columns.empty?
seek_to_record(index)
return nil if deleted_record?
record_data = @data.read(record_length)
DBF::Record.new(record_data, columns, version, @memo)
end
alias row record
# Dumps all records to a CSV file. If no filename is given then CSV is
# output to STDOUT.
#
# @param [optional String] path Defaults to STDOUT
def to_csv(path = nil)
out_io = path ? File.open(path, 'w') : $stdout
csv = CSV.new(out_io, force_quotes: true)
csv << column_names
each { |record| csv << record.to_a }
end
# Human readable version description
#
# @return [String]
def version_description
VERSIONS[version]
end
private
def build_columns # :nodoc:
safe_seek do
@data.seek(header_size)
[].tap do |columns|
until end_of_record?
args = case version
when '02'
[self, *@data.read(header_size * 2).unpack('A11 a C'), 0]
when '04', '8c'
[self, *@data.read(48).unpack('A32 a C C x13')]
else
[self, *@data.read(header_size).unpack('A11 a x4 C2')]
end
columns << Column.new(*args)
end
end
end
end
def header_size
case version
when '02'
DBASE2_HEADER_SIZE
when '04', '8c'
DBASE7_HEADER_SIZE
else
DBASE3_HEADER_SIZE
end
end
def deleted_record? # :nodoc:
flag = @data.read(1)
flag ? flag.unpack1('a') == '*' : true
end
def end_of_record? # :nodoc:
safe_seek { @data.read(1).ord == 13 }
end
def find_all(options) # :nodoc:
select do |record|
next unless record&.match?(options)
yield record if block_given?
record
end
end
def find_first(options) # :nodoc:
detect { |record| record&.match?(options) }
end
def foxpro? # :nodoc:
FOXPRO_VERSIONS.key?(version)
end
def header # :nodoc:
@header ||= safe_seek do
@data.seek(0)
Header.new(@data.read(DBASE3_HEADER_SIZE))
end
end
def memo_class # :nodoc:
@memo_class ||= if foxpro?
Memo::Foxpro
else
version == '83' ? Memo::Dbase3 : Memo::Dbase4
end
end
def memo_search_path(io) # :nodoc:
dirname = File.dirname(io)
basename = File.basename(io, '.*')
"#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
end
def open_data(data) # :nodoc:
data.is_a?(StringIO) ? data : File.open(data, 'rb')
rescue Errno::ENOENT
raise DBF::FileNotFoundError, "file not found: #{data}"
end
def open_memo(data, memo = nil) # :nodoc:
if memo
meth = memo.is_a?(StringIO) ? :new : :open
memo_class.send(meth, memo, version)
elsif !data.is_a?(StringIO)
files = Dir.glob(memo_search_path(data))
files.any? ? memo_class.open(files.first, version) : nil
end
end
def safe_seek # :nodoc:
original_pos = @data.pos
yield.tap { @data.seek(original_pos) }
end
def seek(offset) # :nodoc:
@data.seek(header_length + offset)
end
def seek_to_record(index) # :nodoc:
seek(index * record_length)
end
end
end
dbf-4.3.2/lib/dbf/memo/ 0000755 0000041 0000041 00000000000 14572252217 014550 5 ustar www-data www-data dbf-4.3.2/lib/dbf/memo/dbase3.rb 0000644 0000041 0000041 00000000564 14572252217 016243 0 ustar www-data www-data module DBF
module Memo
class Dbase3 < Base
def build_memo(start_block) # :nodoc:
@data.seek offset(start_block)
memo_string = ''
loop do
block = @data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
memo_string << block
break if block.size < BLOCK_SIZE
end
memo_string
end
end
end
end
dbf-4.3.2/lib/dbf/memo/dbase4.rb 0000644 0000041 0000041 00000000341 14572252217 016235 0 ustar www-data www-data module DBF
module Memo
class Dbase4 < Base
def build_memo(start_block) # :nodoc:
@data.seek offset(start_block)
@data.read(@data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
end
end
end
end
dbf-4.3.2/lib/dbf/memo/base.rb 0000644 0000041 0000041 00000001616 14572252217 016013 0 ustar www-data www-data module DBF
module Memo
class Base
BLOCK_HEADER_SIZE = 8
BLOCK_SIZE = 512
def self.open(filename, version)
new(File.open(filename, 'rb'), version)
end
def initialize(data, version)
@data = data
@version = version
end
def get(start_block)
return nil unless start_block > 0
build_memo start_block
end
def close
@data.close && @data.closed?
end
def closed?
@data.closed?
end
private
def offset(start_block) # :nodoc:
start_block * block_size
end
def content_size(memo_size) # :nodoc:
(memo_size - block_size) + BLOCK_HEADER_SIZE
end
def block_content_size # :nodoc:
@block_content_size ||= block_size - BLOCK_HEADER_SIZE
end
def block_size # :nodoc:
BLOCK_SIZE
end
end
end
end
dbf-4.3.2/lib/dbf/memo/foxpro.rb 0000644 0000041 0000041 00000001354 14572252217 016415 0 ustar www-data www-data module DBF
module Memo
class Foxpro < Base
FPT_HEADER_SIZE = 512
def build_memo(start_block) # :nodoc:
@data.seek offset(start_block)
memo_type, memo_size, memo_string = @data.read(block_size).unpack('NNa*')
return nil unless memo_type == 1 && memo_size > 0
if memo_size > block_content_size
memo_string << @data.read(content_size(memo_size))
else
memo_string = memo_string[0, memo_size]
end
memo_string
rescue StandardError
nil
end
private
def block_size # :nodoc:
@block_size ||= begin
@data.rewind
@data.read(FPT_HEADER_SIZE).unpack1('x6n') || 0
end
end
end
end
end
dbf-4.3.2/lib/dbf/record.rb 0000644 0000041 0000041 00000004751 14572252217 015425 0 ustar www-data www-data module DBF
# An instance of DBF::Record represents a row in the DBF file
class Record
# Initialize a new DBF::Record
#
# @param data [String, StringIO] data
# @param columns [Column]
# @param version [String]
# @param memo [DBF::Memo]
def initialize(data, columns, version, memo)
@data = StringIO.new(data)
@columns = columns
@version = version
@memo = memo
end
# Equality
#
# @param [DBF::Record] other
# @return [Boolean]
def ==(other)
other.respond_to?(:attributes) && other.attributes == attributes
end
# Reads attributes by column name
#
# @param name [String, Symbol] key
def [](name)
key = name.to_s
if attributes.key?(key)
attributes[key]
elsif (index = underscored_column_names.index(key))
attributes[@columns[index].name]
end
end
# Record attributes
#
# @return [Hash]
def attributes
@attributes ||= Hash[column_names.zip(to_a)]
end
# Do all search parameters match?
#
# @param [Hash] options
# @return [Boolean]
def match?(options)
options.all? { |key, value| self[key] == value }
end
# Maps a row to an array of values
#
# @return [Array]
def to_a
@to_a ||= @columns.map { |column| init_attribute(column) }
end
private
def column_names # :nodoc:
@column_names ||= @columns.map(&:name)
end
def get_data(column) # :nodoc:
@data.read(column.length)
end
def get_memo(column) # :nodoc:
if @memo
@memo.get(memo_start_block(column))
else
# the memo file is missing, so read ahead to next record and return nil
@data.read(column.length)
nil
end
end
def init_attribute(column) # :nodoc:
value = column.memo? ? get_memo(column) : get_data(column)
column.type_cast(value)
end
def memo_start_block(column) # :nodoc:
data = get_data(column)
data = data.unpack1('V') if %w[30 31].include?(@version)
data.to_i
end
def method_missing(method, *args) # :nodoc:
if (index = underscored_column_names.index(method.to_s))
attributes[@columns[index].name]
else
super
end
end
def respond_to_missing?(method, *) # :nodoc:
underscored_column_names.include?(method.to_s) || super
end
def underscored_column_names # :nodoc:
@underscored_column_names ||= @columns.map(&:underscored_name)
end
end
end
dbf-4.3.2/lib/dbf/version.rb 0000644 0000041 0000041 00000000052 14572252217 015622 0 ustar www-data www-data module DBF
VERSION = '4.3.2'.freeze
end
dbf-4.3.2/lib/dbf/column_type.rb 0000644 0000041 0000041 00000005130 14572252217 016475 0 ustar www-data www-data module DBF
module ColumnType
class Base
attr_reader :decimal, :encoding
# @param decimal [Integer]
# @param encoding [String, Encoding]
def initialize(decimal, encoding)
@decimal = decimal
@encoding = encoding
end
end
class Nil < Base
# @param _value [String]
def type_cast(_value)
nil
end
end
class Number < Base
# @param value [String]
def type_cast(value)
return nil if value.strip.empty?
@decimal.zero? ? value.to_i : value.to_f
end
end
class Currency < Base
# @param value [String]
def type_cast(value)
(value.unpack1('q<') / 10_000.0).to_f
end
end
class SignedLong < Base
# @param value [String]
def type_cast(value)
value.unpack1('l<')
end
end
class SignedLong2 < Base
# @param value [String]
def type_cast(value)
s = value.unpack1('B*')
sign_multiplier = s[0] == '0' ? -1 : 1
s[1, 31].to_i(2) * sign_multiplier
end
end
class Float < Base
# @param value [String]
def type_cast(value)
value.to_f
end
end
class Double < Base
# @param value [String]
def type_cast(value)
value.unpack1('E')
end
end
class Boolean < Base
# @param value [String]
def type_cast(value)
value.strip.match?(/^(y|t)$/i)
end
end
class Date < Base
# @param value [String]
def type_cast(value)
value.match?(/\d{8}/) && ::Date.strptime(value, '%Y%m%d')
rescue StandardError
nil
end
end
class DateTime < Base
# @param value [String]
def type_cast(value)
days, msecs = value.unpack('l2')
secs = (msecs / 1000).to_i
::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time
rescue StandardError
nil
end
end
class Memo < Base
# @param value [String]
def type_cast(value)
if encoding && !value.nil?
value.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
else
value
end
end
end
class General < Base
# @param value [String]
def type_cast(value)
value
end
end
class String < Base
# @param value [String]
def type_cast(value)
value = value.strip
@encoding ? value.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace) : value
end
end
end
end
dbf-4.3.2/lib/dbf.rb 0000644 0000041 0000041 00000000606 14572252217 014142 0 ustar www-data www-data require 'csv'
require 'date'
require 'forwardable'
require 'json'
require 'time'
require 'dbf/version'
require 'dbf/schema'
require 'dbf/record'
require 'dbf/column_type'
require 'dbf/column'
require 'dbf/encodings'
require 'dbf/header'
require 'dbf/table'
require 'dbf/memo/base'
require 'dbf/memo/dbase3'
require 'dbf/memo/dbase4'
require 'dbf/memo/foxpro'
require 'dbf/database/foxpro'
dbf-4.3.2/spec/ 0000755 0000041 0000041 00000000000 14572252217 013244 5 ustar www-data www-data dbf-4.3.2/spec/dbf/ 0000755 0000041 0000041 00000000000 14572252217 013777 5 ustar www-data www-data dbf-4.3.2/spec/dbf/column_spec.rb 0000644 0000041 0000041 00000021447 14572252217 016643 0 ustar www-data www-data # encoding: ascii-8bit
require 'spec_helper'
RSpec.describe DBF::Column do
let(:table) { DBF::Table.new fixture('dbase_30.dbf') }
context 'when initialized' do
let(:column) { DBF::Column.new table, 'ColumnName', 'N', 1, 0 }
it 'sets :name accessor' do
expect(column.name).to eq 'ColumnName'
end
it 'sets :type accessor' do
expect(column.type).to eq 'N'
end
it 'sets the #length accessor' do
expect(column.length).to eq 1
end
it 'sets the #decimal accessor' do
expect(column.decimal).to eq 0
end
it 'accepts length of 0' do
column = DBF::Column.new table, 'ColumnName', 'N', 0, 0
expect(column.length).to eq 0
end
describe 'with length less than 0' do
it 'raises DBF::Column::LengthError' do
expect { DBF::Column.new table, 'ColumnName', 'N', -1, 0 }.to raise_error(DBF::Column::LengthError)
end
end
describe 'with empty column name' do
it 'raises DBF::Column::NameError' do
expect { DBF::Column.new table, "\xFF\xFC", 'N', 1, 0 }.to raise_error(DBF::Column::NameError)
end
end
end
describe '#type_cast' do
context 'with type N (number)' do
context 'when value is empty' do
it 'returns nil' do
value = ''
column = DBF::Column.new table, 'ColumnName', 'N', 5, 2
expect(column.type_cast(value)).to be_nil
end
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'N', 0, 0
expect(column.type_cast('')).to be_nil
end
end
context 'with 0 decimals' do
it 'casts value to Integer' do
value = '135'
column = DBF::Column.new table, 'ColumnName', 'N', 3, 0
expect(column.type_cast(value)).to eq 135
end
it 'supports negative Integer' do
value = '-135'
column = DBF::Column.new table, 'ColumnName', 'N', 3, 0
expect(column.type_cast(value)).to eq(-135)
end
end
context 'with more than 0 decimals' do
it 'casts value to Float' do
value = '13.5'
column = DBF::Column.new table, 'ColumnName', 'N', 2, 1
expect(column.type_cast(value)).to eq 13.5
end
it 'casts negative value to Float' do
value = '-13.5'
column = DBF::Column.new table, 'ColumnName', 'N', 2, 1
expect(column.type_cast(value)).to eq(-13.5)
end
end
end
context 'with type F (float)' do
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'F', 0, 0
expect(column.type_cast('')).to be_nil
end
end
it 'casts value to Float' do
value = '135'
column = DBF::Column.new table, 'ColumnName', 'F', 3, 0
expect(column.type_cast(value)).to eq 135.0
end
it 'casts negative value to Float' do
value = '-135'
column = DBF::Column.new table, 'ColumnName', 'F', 3, 0
expect(column.type_cast(value)).to eq(-135.0)
end
end
context 'with type B (binary)' do
context 'with Foxpro dbf' do
it 'casts to float' do
column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
expect(column.type_cast("\xEC\x51\xB8\x1E\x85\x6B\x31\x40")).to be_a(Float)
expect(column.type_cast("\xEC\x51\xB8\x1E\x85\x6B\x31\x40")).to eq 17.42
end
it 'stores original precision' do
column = DBF::Column.new table, 'ColumnName', 'B', 1, 0
expect(column.type_cast("\xEC\x51\xB8\x1E\x85\x6B\x31\x40")).to be_a(Float)
expect(column.type_cast("\xEC\x51\xB8\x1E\x85\x6B\x31\x40")).to eq 17.42
end
it 'supports negative binary' do
column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
expect(column.type_cast("\x00\x00\x00\x00\x00\xC0\x65\xC0")).to be_a(Float)
expect(column.type_cast("\x00\x00\x00\x00\x00\xC0\x65\xC0")).to eq(-174.0)
end
end
end
context 'with type I (integer)' do
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'I', 0, 0
expect(column.type_cast('')).to be_nil
end
end
it 'casts value to Integer' do
value = "\203\171\001\000"
column = DBF::Column.new table, 'ColumnName', 'I', 3, 0
expect(column.type_cast(value)).to eq 96_643
end
it 'supports negative Integer' do
value = "\x24\xE1\xFF\xFF"
column = DBF::Column.new table, 'ColumnName', 'I', 3, 0
expect(column.type_cast(value)).to eq(-7900)
end
end
context 'with type L (logical/boolean)' do
let(:column) { DBF::Column.new table, 'ColumnName', 'L', 1, 0 }
it "casts 'y' to true" do
expect(column.type_cast('y')).to be true
end
it "casts 't' to true" do
expect(column.type_cast('t')).to be true
end
it "casts value other than 't' or 'y' to false" do
expect(column.type_cast('n')).to be false
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'L', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
context 'with type T (datetime)' do
let(:column) { DBF::Column.new table, 'ColumnName', 'T', 16, 0 }
context 'with valid datetime' do
it 'casts to DateTime' do
expect(column.type_cast("Nl%\000\300Z\252\003")).to eq Time.parse('2002-10-10T17:04:56+00:00')
end
end
context 'with invalid datetime' do
it 'casts to nil' do
expect(column.type_cast("Nl%\000\000A\000\999")).to be_nil
end
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'T', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
context 'with type D (date)' do
let(:column) { DBF::Column.new table, 'ColumnName', 'D', 8, 0 }
context 'with valid date' do
it 'casts to Date' do
expect(column.type_cast('20050712')).to eq Date.new(2005, 7, 12)
end
end
context 'with invalid date' do
it 'casts to nil' do
expect(column.type_cast('000000000')).to be_nil
end
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'D', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
context 'with type M (memo)' do
it 'casts to string' do
column = DBF::Column.new table, 'ColumnName', 'M', 3, 0
expect(column.type_cast('abc')).to eq 'abc'
end
it 'casts nil to nil' do
column = DBF::Column.new table, 'ColumnName', 'M', 3, 0
expect(column.type_cast(nil)).to be_nil
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'M', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
context 'with type G (memo)' do
it 'returns binary data' do
column = DBF::Column.new table, 'ColumnName', 'G', 3, 0
expect(column.type_cast("\000\013\120")).to eq "\000\013\120"
expect(column.type_cast("\000\013\120").encoding).to eq Encoding::ASCII_8BIT
end
it 'casts nil to nil' do
column = DBF::Column.new table, 'ColumnName', 'G', 3, 0
expect(column.type_cast(nil)).to be_nil
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'G', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
end
context 'with type Y (currency)' do
let(:column) { DBF::Column.new table, 'ColumnName', 'Y', 8, 4 }
it 'casts to float' do
expect(column.type_cast(" \xBF\x02\x00\x00\x00\x00\x00")).to eq 18.0
end
it 'supports negative currency' do
expect(column.type_cast("\xFC\xF0\xF0\xFE\xFF\xFF\xFF\xFF")).to eq(-1776.41)
end
it 'supports 64bit negative currency' do
expect(column.type_cast("pN'9\xFF\xFF\xFF\xFF")).to eq(-333_609.0)
end
context 'with 0 length' do
it 'returns nil' do
column = DBF::Column.new table, 'ColumnName', 'Y', 0, 0
expect(column.type_cast('')).to be_nil
end
end
end
describe '#name' do
it 'contains only ASCII characters' do
column = DBF::Column.new table, "--\x1F-\x68\x65\x6C\x6C\x6F \x00world-\x80--", 'N', 1, 0
expect(column.name).to eq '---hello '
end
it 'is truncated at the null character' do
column = DBF::Column.new table, "--\x1F-\x68\x65\x6C\x6C\x6F \x00world-\x80\x80--", 'N', 1, 0
expect(column.name).to eq '---hello '
end
end
end
dbf-4.3.2/spec/dbf/database/ 0000755 0000041 0000041 00000000000 14572252217 015543 5 ustar www-data www-data dbf-4.3.2/spec/dbf/database/foxpro_spec.rb 0000644 0000041 0000041 00000002773 14572252217 020430 0 ustar www-data www-data require 'spec_helper'
RSpec.describe DBF::Database::Foxpro do
let(:dbf_path) { fixture('foxprodb/FOXPRO-DB-TEST.DBC') }
let(:db) { DBF::Database::Foxpro.new(dbf_path) }
describe '#initialize' do
describe 'when given a path to an existing dbc file' do
it 'does not raise an error' do
expect { DBF::Database::Foxpro.new dbf_path }.to_not raise_error
end
end
describe 'when given a path to a non-existent dbf file' do
it 'raises a DBF::FileNotFound error' do
expect { DBF::Database::Foxpro.new 'x' }.to raise_error(DBF::FileNotFoundError, 'file not found: x')
end
end
describe 'it loads the example db correctly' do
it 'shows a correct list of tables' do
expect(db.table_names.sort).to eq(%w[contacts calls setup types].sort)
end
end
end
describe '#table' do
describe 'when accessing related tables' do
let(:db) { DBF::Database::Foxpro.new(dbf_path) }
it 'loads an existing related table' do
expect(db.contacts.record_count).to eq 5
end
it 'supports a long table field name' do
expect(db.contacts.record(1).spouses_interests).to eq 'Tennis, golf'
end
it 'loads an existing related table with wrong filename casing' do
expect(db.calls.record_count).to eq 16
end
end
end
describe '#table_path' do
it 'returns an absolute path' do
expect(db.table_path('contacts')).to eq File.expand_path('spec/fixtures/foxprodb/contacts.dbf')
end
end
end
dbf-4.3.2/spec/dbf/record_spec.rb 0000644 0000041 0000041 00000007166 14572252217 016626 0 ustar www-data www-data require 'spec_helper'
RSpec.describe DBF::Record do
describe '#to_a' do
let(:table) { DBF::Table.new fixture('dbase_83.dbf') }
let(:record0) { YAML.load_file(fixture('dbase_83_record_0.yml')) }
let(:record9) { YAML.load_file(fixture('dbase_83_record_9.yml')) }
it 'returns an ordered array of attribute values' do
record = table.record(0)
expect(record.to_a).to eq record0
record = table.record(9)
expect(record.to_a).to eq record9
end
describe 'with missing memo file' do
describe 'when opening a path' do
let(:table) { DBF::Table.new fixture('dbase_83_missing_memo.dbf') }
let(:record0) { YAML.load_file(fixture('dbase_83_missing_memo_record_0.yml')) }
it 'returns nil values for memo fields' do
record = table.record(0)
expect(record.to_a).to eq record0
end
end
end
describe 'when opening StringIO' do
let(:data) { StringIO.new(File.read(fixture('dbase_83_missing_memo.dbf'))) }
let(:table) { DBF::Table.new(data) }
let(:record0) { YAML.load_file(fixture('dbase_83_missing_memo_record_0.yml')) }
it 'returns nil values for memo fields' do
record = table.record(0)
expect(record.to_a).to eq record0
end
end
end
describe '#==' do
let(:table) { DBF::Table.new fixture('dbase_8b.dbf') }
let(:record) { table.record(9) }
describe 'when other does not have attributes' do
it 'returns false' do
expect((record == instance_double('DBF::Record'))).to be_falsey
end
end
describe 'if other attributes match' do
let(:attributes) { {x: 1, y: 2} }
let(:other) { instance_double('DBF::Record', attributes: attributes) }
before do
allow(record).to receive(:attributes).and_return(attributes)
end
it 'returns true' do
expect(record == other).to be_truthy
end
end
end
describe 'column accessors' do
let(:table) { DBF::Table.new fixture('dbase_8b.dbf') }
let(:record) { table.find(0) }
%w[character numerical date logical float memo].each do |column_name|
it "defines accessor method for '#{column_name}' column" do
expect(record).to respond_to(column_name.to_sym)
end
end
end
describe 'column data for table' do
describe 'using specified in dbf encoding' do
let(:table) { DBF::Table.new fixture('cp1251.dbf') }
let(:record) { table.find(0) }
it 'encodes to default system encoding' do
expect(record.name.encoding).to eq Encoding.default_external
# russian a
expect(record.name.encode('UTF-8').unpack1('H4')).to eq 'd0b0'
end
end
describe 'overriding specified in dbf encoding' do
let(:table) { DBF::Table.new fixture('cp1251.dbf'), nil, 'cp866' }
let(:record) { table.find(0) }
it 'transcodes from manually specified encoding to default system encoding' do
expect(record.name.encoding).to eq Encoding.default_external
# russian а encoded in cp1251 and read as if it was encoded in cp866
expect(record.name.encode('UTF-8').unpack1('H4')).to eq 'd180'
end
end
end
describe '#attributes' do
let(:table) { DBF::Table.new fixture('dbase_8b.dbf') }
let(:record) { table.find(0) }
it 'is a hash of attribute name/value pairs' do
expect(record.attributes).to be_a(Hash)
expect(record.attributes['CHARACTER']).to eq 'One'
end
it 'has only original field names as keys' do
original_field_names = %w[CHARACTER DATE FLOAT LOGICAL MEMO NUMERICAL]
expect(record.attributes.keys.sort).to eq original_field_names
end
end
end
dbf-4.3.2/spec/dbf/file_formats_spec.rb 0000644 0000041 0000041 00000013172 14572252217 020014 0 ustar www-data www-data require 'spec_helper'
RSpec.shared_examples_for 'DBF' do
let(:header_record_length) { table.instance_eval { header.record_length } }
let(:sum_of_column_lengths) { table.columns.inject(1) { |sum, column| sum + column.length } }
specify 'sum of column lengths should equal record length specified in header plus one' do
expect(header_record_length).to eq sum_of_column_lengths
end
specify 'records should be instances of DBF::Record' do
expect(table).to all be_kind_of(DBF::Record)
end
specify 'record count should be the same as reported in the header' do
expect(table.entries.size).to eq table.record_count
end
specify 'column names should not be blank' do
table.columns.each do |column|
expect(column.name).to_not be_empty
end
end
specify 'column types should be valid' do
valid_column_types = %w[C N L D M F B G P Y T I V X @ O + 0]
table.columns.each do |column|
expect(valid_column_types).to include(column.type)
end
end
specify 'column lengths should be instances of Integer' do
table.columns.each do |column|
expect(column.length).to be_kind_of(Integer)
end
end
specify 'column lengths should be larger than 0' do
table.columns.each do |column|
expect(column.length).to be > 0
end
end
specify 'column decimals should be instances of Integer' do
table.columns.each do |column|
expect(column.decimal).to be_kind_of(Integer)
end
end
end
RSpec.describe DBF, 'of type 02 (FoxBase)' do
let(:table) { DBF::Table.new fixture('dbase_02.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '02'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'FoxBase'
end
it 'determines the number of records' do
expect(table.record_count).to eq 9
end
end
RSpec.describe DBF, 'of type 03 (dBase III without memo file)' do
let(:table) { DBF::Table.new fixture('dbase_03.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '03'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'dBase III without memo file'
end
it 'determines the number of records' do
expect(table.record_count).to eq 14
end
end
RSpec.describe DBF, 'of type 30 (Visual FoxPro)' do
let(:table) { DBF::Table.new fixture('dbase_30.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '30'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'Visual FoxPro'
end
it 'determines the number of records' do
expect(table.record_count).to eq 34
end
it 'reads memo data' do
expect(table.record(3).classes).to match(/\AAgriculture.*Farming\r\n\Z/m)
end
end
RSpec.describe DBF, 'of type 31 (Visual FoxPro with AutoIncrement field)' do
let(:table) { DBF::Table.new fixture('dbase_31.dbf') }
it_behaves_like 'DBF'
it 'has a dBase version of 31' do
expect(table.version).to eq '31'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'Visual FoxPro with AutoIncrement field'
end
it 'determines the number of records' do
expect(table.record_count).to eq 77
end
end
RSpec.describe DBF, 'of type 32 (Visual FoxPro with field type Varchar or Varbinary)' do
let(:table) { DBF::Table.new fixture('dbase_32.dbf') }
it_behaves_like 'DBF'
it 'has a dBase version of 32' do
expect(table.version).to eq '32'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'Visual FoxPro with field type Varchar or Varbinary'
end
it 'determines the number of records' do
expect(table.record_count).to eq 1
end
end
RSpec.describe DBF, 'of type 83 (dBase III with memo file)' do
let(:table) { DBF::Table.new fixture('dbase_83.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '83'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'dBase III with memo file'
end
it 'determines the number of records' do
expect(table.record_count).to eq 67
end
end
RSpec.describe DBF, 'of type 8b (dBase IV with memo file)' do
let(:table) { DBF::Table.new fixture('dbase_8b.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '8b'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'dBase IV with memo file'
end
it 'determines the number of records' do
expect(table.record_count).to eq 10
end
end
RSpec.describe DBF, 'of type 8c (unknown)' do
let(:table) { DBF::Table.new fixture('dbase_8c.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq '8c'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'dBase 7'
end
it 'determines the number of records' do
expect(table.record_count).to eq 10
end
end
RSpec.describe DBF, 'of type f5 (FoxPro with memo file)' do
let(:table) { DBF::Table.new fixture('dbase_f5.dbf') }
it_behaves_like 'DBF'
it 'reports the correct version number' do
expect(table.version).to eq 'f5'
end
it 'reports the correct version description' do
expect(table.version_description).to eq 'FoxPro with memo file'
end
it 'determines the number of records' do
expect(table.record_count).to eq 975
end
it 'reads memo data' do
expect(table.record(3).datn.to_s).to eq '1870-06-30'
end
end
dbf-4.3.2/spec/dbf/table_spec.rb 0000644 0000041 0000041 00000026507 14572252217 016437 0 ustar www-data www-data require 'spec_helper'
RSpec.describe DBF::Table do
let(:dbf_path) { fixture('dbase_83.dbf') }
let(:memo_path) { fixture('dbase_83.dbt') }
let(:table) { DBF::Table.new dbf_path }
specify 'foxpro versions' do
expect(DBF::Table::FOXPRO_VERSIONS.keys.sort).to eq %w[30 31 f5 fb].sort
end
specify 'row is an alias of record' do
expect(table.record(1)).to eq table.row(1)
end
describe '#initialize' do
let(:data) { StringIO.new File.read(dbf_path) }
let(:memo) { StringIO.new File.read(memo_path) }
describe 'when given a path to an existing dbf file' do
it 'does not raise an error' do
expect { DBF::Table.new dbf_path }.to_not raise_error
end
end
describe 'when given a path to a non-existent dbf file' do
it 'raises a DBF::FileNotFound error' do
expect { DBF::Table.new 'x' }.to raise_error(DBF::FileNotFoundError, 'file not found: x')
end
end
describe 'when given paths to existing dbf and memo files' do
it 'does not raise an error' do
expect { DBF::Table.new dbf_path, memo_path }.to_not raise_error
end
end
it 'accepts an io-like data object' do
expect { DBF::Table.new data }.to_not raise_error
end
it 'accepts an io-like data and memo object' do
expect { DBF::Table.new data, memo }.to_not raise_error
end
end
describe '#close' do
before { table.close }
it 'closes the io' do
expect { table.record(1) }.to raise_error(IOError)
end
end
describe '#schema' do
describe 'when data is IO' do
let(:control_schema) { File.read(fixture('dbase_83_schema_ar.txt')) }
it 'matches the test schema fixture' do
expect(table.schema).to eq control_schema
end
it 'raises ArgumentError if there is no matching schema' do
expect { table.schema(:invalid) }.to raise_error(
ArgumentError,
':invalid is not a valid schema. Valid schemas are: activerecord, json, sequel.'
)
end
end
describe 'when data is StringIO' do
let(:data) { StringIO.new File.read(dbf_path) }
let(:table) { DBF::Table.new data }
let(:control_schema) { File.read(fixture('dbase_83_schema_ar.txt')) }
it 'matches the test schema fixture' do
table.name = 'dbase_83'
expect(table.schema).to eq control_schema
end
end
end
describe '#sequel_schema' do
it 'returns a valid Sequel migration by default' do
control_schema = File.read(fixture('dbase_83_schema_sq.txt'))
expect(table.sequel_schema).to eq control_schema
end
it 'returns a limited Sequel migration when passed true' do
control_schema = File.read(fixture('dbase_83_schema_sq_lim.txt'))
expect(table.sequel_schema).to eq control_schema
end
end
describe '#json_schema' do
it 'is valid JSON' do
expect { JSON.parse(table.json_schema) }.to_not raise_error
end
it 'matches the test fixture' do
data = JSON.parse(table.json_schema)
expect(data).to eq [
{'name' => 'ID', 'type' => 'N', 'length' => 19, 'decimal' => 0},
{'name' => 'CATCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
{'name' => 'AGRPCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
{'name' => 'PGRPCOUNT', 'type' => 'N', 'length' => 19, 'decimal' => 0},
{'name' => 'ORDER', 'type' => 'N', 'length' => 19, 'decimal' => 0},
{'name' => 'CODE', 'type' => 'C', 'length' => 50, 'decimal' => 0},
{'name' => 'NAME', 'type' => 'C', 'length' => 100, 'decimal' => 0},
{'name' => 'THUMBNAIL', 'type' => 'C', 'length' => 254, 'decimal' => 0},
{'name' => 'IMAGE', 'type' => 'C', 'length' => 254, 'decimal' => 0},
{'name' => 'PRICE', 'type' => 'N', 'length' => 13, 'decimal' => 2},
{'name' => 'COST', 'type' => 'N', 'length' => 13, 'decimal' => 2},
{'name' => 'DESC', 'type' => 'M', 'length' => 10, 'decimal' => 0},
{'name' => 'WEIGHT', 'type' => 'N', 'length' => 13, 'decimal' => 2},
{'name' => 'TAXABLE', 'type' => 'L', 'length' => 1, 'decimal' => 0},
{'name' => 'ACTIVE', 'type' => 'L', 'length' => 1, 'decimal' => 0}
]
end
end
describe '#to_csv' do
after do
FileUtils.rm_f 'test.csv'
end
describe 'when no path param passed' do
it 'writes to STDOUT' do
expect { table.to_csv }.to output.to_stdout
end
end
describe 'when path param passed' do
before { table.to_csv('test.csv') }
it 'creates a custom csv file' do
expect(File).to be_exist('test.csv')
end
end
end
describe '#record' do
it 'return nil for deleted records' do
allow(table).to receive(:deleted_record?).and_return(true)
expect(table.record(5)).to be_nil
end
describe 'when dbf has no column definitions' do
let(:dbf_path) { fixture('polygon.dbf') }
it 'raises a DBF::NoColumnsDefined error' do
expect { DBF::Table.new(dbf_path).record(1) }.to raise_error(DBF::NoColumnsDefined, 'The DBF file has no columns defined')
end
end
end
describe '#current_record' do
it 'returns nil for deleted records' do
allow(table).to receive(:deleted_record?).and_return(true)
expect(table.record(0)).to be_nil
end
end
describe '#find' do
describe 'with index' do
it 'returns the correct record' do
expect(table.find(5)).to eq table.record(5)
end
end
describe 'with array of indexes' do
it 'returns the correct records' do
expect(table.find([1, 5, 10])).to eq [table.record(1), table.record(5), table.record(10)]
end
end
describe 'with :all' do
let(:records) do
table.find(:all, weight: 0.0)
end
it 'retrieves only matching records' do
expect(records.size).to eq 66
end
it 'yields to a block if given' do
record_count = 0
table.find(:all, weight: 0.0) do |record|
record_count += 1
expect(record).to be_a DBF::Record
end
expect(record_count).to eq 66
end
it 'returns all records if options are empty' do
expect(table.find(:all)).to eq table.to_a
end
it 'returns matching records when used with options' do
expect(table.find(:all, 'WEIGHT' => 0.0)).to eq(table.select { |r| r['weight'] == 0.0 })
end
it 'ANDS multiple search terms' do
expect(table.find(:all, 'ID' => 30, :IMAGE => 'graphics/00000001/TBC01.jpg')).to be_empty
end
it 'matches original column names' do
expect(table.find(:all, 'WEIGHT' => 0.0)).to_not be_empty
end
it 'matches symbolized column names' do
expect(table.find(:all, WEIGHT: 0.0)).to_not be_empty
end
it 'matches downcased column names' do
expect(table.find(:all, 'weight' => 0.0)).to_not be_empty
end
it 'matches symbolized downcased column names' do
expect(table.find(:all, weight: 0.0)).to_not be_empty
end
end
describe 'with :first' do
it 'returns the first record if options are empty' do
expect(table.find(:first)).to eq table.record(0)
end
it 'returns the first matching record when used with options' do
expect(table.find(:first, 'CODE' => 'C')).to eq table.record(5)
end
it 'ANDs multiple search terms' do
expect(table.find(:first, 'ID' => 30, 'IMAGE' => 'graphics/00000001/TBC01.jpg')).to be_nil
end
end
end
describe '#filename' do
it 'returns the filename as a string' do
expect(table.filename).to eq 'dbase_83.dbf'
end
end
describe '#name' do
describe 'when data is an IO' do
it 'defaults to the filename less extension' do
expect(table.name).to eq 'dbase_83'
end
it 'is mutable' do
table.name = 'database_83'
expect(table.name).to eq 'database_83'
end
end
describe 'when data is a StringIO' do
let(:data) { StringIO.new File.read(dbf_path) }
let(:memo) { StringIO.new File.read(memo_path) }
let(:table) { DBF::Table.new data }
it 'is nil' do
expect(table.name).to be_nil
end
it 'is mutable' do
table.name = 'database_83'
expect(table.name).to eq 'database_83'
end
end
end
describe '#has_memo_file?' do
describe 'without a memo file' do
let(:table) { DBF::Table.new fixture('dbase_03.dbf') }
it 'is false' do
expect(table).to_not have_memo_file
end
end
describe 'with a memo file' do
it 'is true' do
expect(table).to have_memo_file
end
end
end
describe '#columns' do
let(:columns) { table.columns }
it 'is an array of Columns' do
expect(columns).to be_an(Array)
expect(columns).to_not be_empty
expect(columns).to(be_all { |c| c.is_a? DBF::Column })
end
end
describe '#column_names' do
let(:column_names) do
%w[ID CATCOUNT AGRPCOUNT PGRPCOUNT ORDER CODE NAME THUMBNAIL IMAGE PRICE COST DESC WEIGHT TAXABLE ACTIVE]
end
describe 'when data is an IO' do
it 'is an array of all column names' do
expect(table.column_names).to eq column_names
end
end
describe 'when data is a StringIO' do
let(:data) { StringIO.new File.read(dbf_path) }
let(:table) { DBF::Table.new data, nil, Encoding::US_ASCII }
it 'is an array of all column names' do
expect(table.column_names).to eq column_names
end
end
end
describe '#activerecord_schema_definition' do
context 'with type N (number)' do
it 'outputs an integer column' do
column = DBF::Column.new table, 'ColumnName', 'N', 1, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :integer\n"
end
end
describe 'with type B (binary)' do
context 'with Foxpro dbf' do
it 'outputs a float column' do
column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :binary\n"
end
end
end
it 'defines a float colmn if type is (N)umber with more than 0 decimals' do
column = DBF::Column.new table, 'ColumnName', 'N', 1, 2
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :float\n"
end
it 'defines a date column if type is (D)ate' do
column = DBF::Column.new table, 'ColumnName', 'D', 8, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :date\n"
end
it 'defines a datetime column if type is (D)ate' do
column = DBF::Column.new table, 'ColumnName', 'T', 16, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :datetime\n"
end
it 'defines a boolean column if type is (L)ogical' do
column = DBF::Column.new table, 'ColumnName', 'L', 1, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :boolean\n"
end
it 'defines a text column if type is (M)emo' do
column = DBF::Column.new table, 'ColumnName', 'M', 1, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :text\n"
end
it 'defines a string column with length for any other data types' do
column = DBF::Column.new table, 'ColumnName', 'X', 20, 0
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :string, :limit => 20\n"
end
end
end
dbf-4.3.2/spec/spec_helper.rb 0000644 0000041 0000041 00000001162 14572252217 016062 0 ustar www-data www-data begin
require 'simplecov'
SimpleCov.start
rescue LoadError
# ignore
end
require 'dbf'
require 'yaml'
require 'rspec'
require 'fileutils'
RSpec.configure do |config|
config.disable_monkey_patching!
config.warnings = true
config.order = :random
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
def fixture_path
@fixture_path ||= File.join(File.dirname(__FILE__), 'fixtures')
end
def fixture(filename)
File.join(fixture_path, filename)
end
dbf-4.3.2/spec/fixtures/ 0000755 0000041 0000041 00000000000 14572252217 015115 5 ustar www-data www-data dbf-4.3.2/spec/fixtures/dbase_32_summary.txt 0000644 0000041 0000041 00000000535 14572252217 021020 0 ustar www-data www-data
Database: dbase_32.dbf
Type: (32) Visual FoxPro with field type Varchar or Varbinary
Memo File: false
Records: 1
Fields:
Name Type Length Decimal
------------------------------------------------------------------------------
NAME V 250 0
_NullFlags 0 1 0
dbf-4.3.2/spec/fixtures/foxprodb/ 0000755 0000041 0000041 00000000000 14572252217 016740 5 ustar www-data www-data dbf-4.3.2/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DBC 0000644 0000041 0000041 00000023613 14572252217 021334 0 ustar www-data www-data 0: ( OBJECTID I PARENTID I OBJECTTYPE C
OBJECTNAME C PROPERTY M CODE M RIINFO C USER M
Database Database Database TransactionLog Database StoredProceduresSource Database StoredProceduresObject R Database StoredProceduresDependencies Table types Field contact_type_id Field contact_type Table setup
Field key_name Field value Table contacts
Field contact_id Field first_name Field last_name Field dear Field address Field city Field state Field postalcode Field region Field country Field company_name Field title Field work_phone Field work_extension Field home_phone Field mobile_phone Field fax_number Field email_name Field birthdate Field last_meeting ! Field contact_type_id " Field referred_by # Field notes $ Field marital_status % Field spouse_name &