defmodule DBF.Database do alias DBF.Memo alias DBF.DatabaseError defstruct [ :device, :filename, :memo_file, version: 0, options: [], last_updated: Date.from_erl!({1900, 01, 01}), number_of_records: 0, header_bytes: 0, record_bytes: 0, fields: [], position: 0 ] @type t :: %__MODULE__{ device: pid() | {:file_descriptor, atom(), any()}, filename: String.t(), memo_file: Memo.t() | false, version: byte(), options: DBF.options(), last_updated: Date.t(), number_of_records: integer, header_bytes: integer, record_bytes: integer, fields: [DBF.Field.t()], position: integer } @versions %{ 0x02 => "FoxBase", 0x03 => "dBase III without memo file", 0x04 => "dBase IV without memo file", 0x05 => "dBase V without memo file", 0x07 => "Visual Objects 1.x", 0x30 => "Visual FoxPro", 0x31 => "Visual FoxPro with AutoIncrement field", 0x32 => "Visual FoxPro with field type Varchar or Varbinary", 0x43 => "dBASE IV SQL table files, no memo", 0x63 => "dBASE IV SQL system files, no memo", 0x7b => "dBase IV with memo file", 0x83 => "dBase III with memo file", 0x87 => "Visual Objects 1.x with memo file", 0x8b => "dBase IV with memo file", 0x8e => "dBase IV with SQL table", 0xcb => "dBASE IV SQL table files, with memo", 0xf5 => "FoxPro with memo file", 0xe5 => "HiPer-Six format with SMT memo file", 0xfb => "FoxPro without memo file" } @foxpro_versions [0x30, 0x31, 0x32, 0xf5, 0xfb] @supported_versions [0x02, 0x03, 0x83, 0x8b] def open_database(%__MODULE__{}=db) do with {:ok, db} <- read_version(db), {:ok, db} <- read_header(db) do {:ok, db} end end defp read_header(%__MODULE__{version: 0x02, device: device}=db) do {:ok, data} = :file.pread(device, 0, 8) << _version::unsigned-integer-8, records::little-unsigned-integer-16, _unknown::binary-size(3), record_length::little-unsigned-integer-16, >> = data {:ok, %__MODULE__{ db | last_updated: Date.from_erl!({1900, 01, 01}), number_of_records: records, header_bytes: 521, record_bytes: record_length } } end defp read_header(%__MODULE__{device: device}=db) do {:ok, data} = :file.pread(device, 0, 32) << _version::unsigned-integer-8, year::unsigned-integer-8, month::unsigned-integer-8, day::unsigned-integer-8, records::little-unsigned-integer-32, header_length::little-unsigned-integer-16, record_length::little-unsigned-integer-16, _reserved1::binary-size(2), _transaction::binary-size(1), _reserved2::binary-size(12), _table_flags::binary-size(1), _code_page_mark::binary-size(1), _reserved3::binary-size(2), _header_terminator::binary-size(1) >> = data {:ok, %__MODULE__{ db | last_updated: Date.from_erl!({year + 1900, month, day}), number_of_records: records, header_bytes: header_length, record_bytes: record_length } } end @doc false @spec foxpro?(DBF.Database.t()) :: boolean() def foxpro?(%__MODULE__{version: version}) do version in @foxpro_versions end @doc false @spec well_known_version?(DBF.Database.t()) :: boolean() def well_known_version?(%__MODULE__{version: version}) do Map.has_key?(@versions, version) end defp read_version(db) do {:ok, <>} = :file.pread(db.device, 0, 1) if version in @supported_versions do {:ok, %__MODULE__{db | version: version}} else version_string = Map.get(@versions, version, Integer.to_string(version)) {:error, DatabaseError.new(:unsupported_version, version_string)} end end end # Define the Enumerable implentation for the database. defimpl Enumerable, for: DBF.Database do @spec count(DBF.Database.t()) :: {:ok, any()} def count(db) do {:ok, db.number_of_records} end @spec reduce(DBF.Database.t(), {:cont, any()} | {:halt, any()} | {:suspend, any()}, any()) :: {:done, any()} | {:halted, any()} | {:suspended, any(), ({any(), any()} -> {any(), any()} | {any(), any(), any()})} def reduce(db, {:cont, acc}, fun) do if (db.position == db.number_of_records) do {:done, acc} else record = DBF.get(db, db.position) reduce(Map.put(db, :position, db.position + 1), fun.(record, acc), fun) end end def reduce(_db, {:halt, acc}, _fun) do {:halted, acc} end def reduce(db, {:suspend, acc}, fun) do {:suspended, acc, &reduce(db, &1, fun)} end @spec slice(DBF.Database.t()) :: {:error, Enumerable.DBF.Database} def slice(_array) do {:error, __MODULE__} end @spec member?(DBF.Database.t(), any()) :: {:error, Enumerable.DBF.Database} def member?(_array, _element) do {:error, __MODULE__} end end