Skip to content

Commit

Permalink
Fix decimal "C" type read, add support field type "@", fix read Datet…
Browse files Browse the repository at this point in the history
…ime for "T", "@" field types.
  • Loading branch information
nchizhov committed Dec 22, 2024
1 parent 51e1561 commit d04ce66
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 69 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf

[*.md]
max_line_length = off
trim_trailing_whitespace = false
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This group of classes (**Table**, **Records**, **Memo** in namespace **Inok\Dbf*
May read headers of: FoxBASE, dBASE III, dBASE IV, dBASE 5, dBASE 7 (*partial*), FoxPro, FoxBASE+, Visual FoxPro file structure.

##### Using:
```
```php
$table = new \Inok\Dbf\Table(/path/to/dbf/file, $charset);
```
where `$charset` only using, when charset in dbf-file not defined (default charset: **866**)
Expand All @@ -37,12 +37,28 @@ May read records of: FoxBASE, dBASE III, dBASE IV, dBASE 5, dBASE 7, FoxPro, Fox
* **N** - Numeric (if empty converts to null)
* **P** - Picture
* **T** - DateTime as string in format 'YYYYMMDDHHIISS' (if empty converts to null)
* **@** - DateTime as string in format 'YYYYMMDDHHIISS' (if empty converts to null)
* **I** - Integer
* **Y** - Currency
* **0** - NullFlags as integer

Not supported types:
* **B**:
- dBase 5 (Binary) - block num in MEMO-file (10 digits, padded right with spaces). Empty value: 10 spaces
- Visual FoxPro (Double) - float 8-byte binary format [IEEE 754](https://ru.wikipedia.org/wiki/IEEE_754). Empty values - zero
* **O**:
- dBase 7 (Double) - float 8-byte format [IEEE 754](https://ru.wikipedia.org/wiki/IEEE_754). Bytes inverse, for negative numbers - inverse all bits, for positive - sign bit only. Empty values - zero
* **Q**:
- Visual FoxPro (Varbinary) - Binary data with variable data. Start part saved in DBF-file, other part with variable lenth - in MEMO-file
* **V**:
- Visual FoxPro (Varchar) - String with variable length. Start part saved in DBS-file, other part with variable length - in MEMO-file.
* **W**
- Visual FoxPro (Blob) - No info about format
* **+**
- dBase 7 (Autoincrement) - Signed integer in binary format. Length - 4 bytes BE.

##### Using:
```
```php
$records = new \Inok\Dbf\Records($data, $encode, $headers, $columns);
```
* **$data** - Instance of Table class or DBF-file resource from Inok\Dbf\Table getData()
Expand All @@ -57,7 +73,7 @@ $records = new \Inok\Dbf\Records($data, $encode, $headers, $columns);
May read MEMO-files formats (headers and records): DBT, FPT, SMT

##### Using:
```
```php
$memo = new \Inok\Dbf\Memo(/path/to/dbf/memo/file);
```

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"name": "inok/dbf",
"description": "Package for reading DBASE-files (FoxPro) with/without MEMO-fields",
"keywords": ["dbf", "memo", "foxpro", "dbase"],
"homepage": "http://blog.kgd.in",
"homepage": "https://blog.kgd.in",
"type": "library",
"license": "MIT",
"version": "1.0.7",
"version": "1.0.8",
"authors": [
{
"name": "Chizhov Nikolay",
"email": "[email protected]",
"homepage": "http://blog.kgd.in",
"homepage": "https://blog.kgd.in",
"role": "System Administrator"
}
],
Expand Down
59 changes: 34 additions & 25 deletions src/Memo.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* DBF-file MEMO-fields Reader
*
* Author: Chizhov Nikolay <[email protected]>
* (c) 2019 CIOB "Inok"
* (c) 2019-2024 CIOB "Inok"
********************************************/

namespace Inok\Dbf;

use Exception;

class Memo {
private $headers = null;

Expand All @@ -21,6 +23,9 @@ class Memo {
private $isBase4 = false;
private $isBase3 = false;

/**
* @throws Exception
*/
public function __construct($file) {
$this->db = $file;

Expand All @@ -31,9 +36,12 @@ public function __destruct() {
$this->close();
}

/**
* @throws Exception
*/
private function open() {
if (!file_exists($this->db)) {
throw new \Exception(sprintf('Memo-file %s cannot be found', $this->db));
throw new Exception(sprintf('Memo-file %s cannot be found', $this->db));
}
$this->fp = fopen($this->db, "rb");
}
Expand Down Expand Up @@ -75,18 +83,20 @@ private function readHeaders() {
"freeblock_position" => unpack("L", substr($data, 0, 4))[1],
"block_size" => 512
];
} elseif ($this->isBase4) {
return;
}
if ($this->isBase4) {
$this->headers = [
"freeblock_position" => unpack("L", substr($data, 0, 4))[1],
"block_size" => unpack("S", substr($data, 20, 2))[1],
"dbf-file" => $fileName
];
} else {
$this->headers = [
"freeblock_position" => unpack("N", substr($data, 0, 4))[1],
"block_size" => unpack("n", substr($data, 6, 2))[1]
];
return;
}
$this->headers = [
"freeblock_position" => unpack("N", substr($data, 0, 4))[1],
"block_size" => unpack("n", substr($data, 6, 2))[1]
];
}

private function readMemo($block) {
Expand All @@ -97,22 +107,22 @@ private function readMemo($block) {
$text .= fread($this->fp, 512);
}
$memo["text"] = $this->parseDBase3($text);
} else {
$data = fread($this->fp, 8);
if ($this->isBase4) {
$memo = [
"signature" => $this->signature[unpack("N", substr($data, 0, 4))[1]],
"length" => octdec(intval(bin2hex(trim(substr($data, 4, 4)))))
];
$memo["text"] = $this->parseDBase4(fread($this->fp, $memo["length"]));
} else {
$memo = [
"signature" => $this->signature[unpack("N", substr($data, 0, 4))[1]],
"length" => unpack("N", substr($data, 4, 4))[1]
];
$memo["text"] = fread($this->fp, $memo["length"]);
}
return $memo;
}
$data = fread($this->fp, 8);
if ($this->isBase4) {
$memo = [
"signature" => $this->signature[unpack("N", substr($data, 0, 4))[1]],
"length" => octdec(intval(bin2hex(trim(substr($data, 4, 4)))))
];
$memo["text"] = $this->parseDBase4(fread($this->fp, $memo["length"]));
return $memo;
}
$memo = [
"signature" => $this->signature[unpack("N", substr($data, 0, 4))[1]],
"length" => unpack("N", substr($data, 4, 4))[1]
];
$memo["text"] = fread($this->fp, $memo["length"]);
return $memo;
}

Expand All @@ -127,7 +137,6 @@ private function parseDBase4($text) {
if (preg_match('/\x0d\x0a/', $text, $matches, PREG_OFFSET_CAPTURE)) {
$text = substr($text, 0, $matches[0][1]);
}
$text = preg_replace('/\x8d\x0a/', "\n", $text);
return $text;
return preg_replace('/\x8d\x0a/', "\n", $text);
}
}
16 changes: 13 additions & 3 deletions src/Records.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* DBF-file records Reader
*
* Author: Chizhov Nikolay <[email protected]>
* (c) 2019 CIOB "Inok"
* (c) 2019-2024 CIOB "Inok"
********************************************/

namespace Inok\Dbf;

use Exception;

class Records {
private $fp, $headers, $columns, $memo, $encode;
private $records = 0;
Expand All @@ -18,6 +20,9 @@ class Records {
private $logicals = ['t', 'y', 'д'];
private $notTrimTypes = ["M", "P", "G", "I", "Y", "T", "0"];

/**
* @throws Exception
*/
public function __construct($data, $encode = "utf-8", $headers = null, $columns = null) {
if ($data instanceof Table) {
$this->headers = $data->getHeaders();
Expand All @@ -26,7 +31,7 @@ public function __construct($data, $encode = "utf-8", $headers = null, $columns
}
else {
if (is_null($headers) || is_null($columns)) {
throw new \Exception('Not correct data in Record class');
throw new Exception('Not correct data in Record class');
}
$this->fp = $data;
$this->headers = $headers;
Expand Down Expand Up @@ -64,6 +69,7 @@ public function nextRecord() {
case "I":
$record[$column["name"]] = unpack("l", $sub_data)[1];
break;
case "@":
case "T":
$record[$column["name"]] = $this->getDateTime($sub_data);
break;
Expand Down Expand Up @@ -119,9 +125,13 @@ private function convertChar($data) {
}

private function getDateTime($data) {
if (empty(trim($data))) {
$data = trim($data);
if (empty($data)) {
return null;
}
if (strlen($data) == 14) {
return $data;
}
$dateData = unpack("L", substr($data, 0, 4))[1];
$timeData = unpack("L", substr($data, 4, 4))[1];
return gmdate("YmdHis", jdtounix($dateData) + intval($timeData / 1000));
Expand Down
74 changes: 39 additions & 35 deletions src/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* DBF-file Structure Reader
*
* Author: Chizhov Nikolay <[email protected]>
* (c) 2019 CIOB "Inok"
* (c) 2019-2024 CIOB "Inok"
********************************************/

namespace Inok\Dbf;

use Exception;

class Table {
private $headers = null;
private $columns = null;
Expand Down Expand Up @@ -61,11 +63,14 @@ class Table {
];
private $dbase7 = false, $v_foxpro = false;

/**
* @throws Exception
*/
public function __construct($dbPath, $charset = null){
$this->db = $dbPath;
if (!is_null($charset)) {
if (!is_numeric($charset)) {
throw new \Exception("Set not correct charset. Allows only digits.");
throw new Exception("Set not correct charset. Allows only digits.");
}
$this->charsets[0] = $charset;
}
Expand All @@ -76,9 +81,12 @@ public function __destruct() {
$this->close();
}

/**
* @throws Exception
*/
private function open() {
if (!file_exists($this->db)) {
throw new \Exception(sprintf('File %s cannot be found', $this->db));
throw new Exception(sprintf('File %s cannot be found', $this->db));
}
$this->fp = fopen($this->db, "rb");
}
Expand Down Expand Up @@ -132,34 +140,31 @@ private function readHeaders() {
if ($this->headers["checks"][0] != 0) {
$this->error = true;
$this->error_info = "Not correct DBF file by headers";
return;
}
$this->headers["charset_name"] = "cp" . $this->charsets[$this->headers["charset"]];

if (in_array("dBASE 7", $this->versions[$this->headers["version"]])) {
$this->dbase7 = true;
$this->headers["columns"] = ($this->headers["header_length"] - 68) / 48;
} elseif (in_array("Visual FoxPro", $this->versions[$this->headers["version"]])) {
$this->v_foxpro = true;
$this->headers["memo"] = (in_array($this->headers["mdx_flag"], [2, 3, 6, 7]));
$this->headers["columns"] = ($this->headers["header_length"] - 296) / 32;
} else {
$this->headers["columns"] = ($this->headers["header_length"] - 33) / 32;
}
else {
$this->headers["charset_name"] = "cp".$this->charsets[$this->headers["charset"]];

if (in_array("dBASE 7", $this->versions[$this->headers["version"]])) {
$this->dbase7 = true;
$this->headers["columns"] = ($this->headers["header_length"] - 68) / 48;
}
elseif (in_array("Visual FoxPro", $this->versions[$this->headers["version"]])) {
$this->v_foxpro = true;
$this->headers["memo"] = (in_array($this->headers["mdx_flag"], [2, 3, 6, 7]));
$this->headers["columns"] = ($this->headers["header_length"] - 296) / 32;
}
else {
$this->headers["columns"] = ($this->headers["header_length"] - 33) / 32;
}

if (!isset($this->headers["memo"])) {
$this->headers["memo"] = in_array($this->headers["version"], $this->memo["versions"]);
}
if ($this->headers["memo"]) {
$this->headers["memo_file"] = ($mfile = $this->getMemoFile($file["dirname"]."/".$file["filename"])) ? $mfile : null;
}

$this->headers["version_name"] =
implode(", ", $this->versions[$this->headers["version"]])." ".($this->headers["memo"] ? "with" : "without")." memo-fields";
unset($this->headers["checks"], $this->headers["header_length"]);
if (!isset($this->headers["memo"])) {
$this->headers["memo"] = in_array($this->headers["version"], $this->memo["versions"]);
}
if ($this->headers["memo"]) {
$this->headers["memo_file"] = ($mfile = $this->getMemoFile($file["dirname"] . "/" . $file["filename"])) ? $mfile : null;
}

$this->headers["version_name"] =
implode(", ", $this->versions[$this->headers["version"]]) . " " . ($this->headers["memo"] ? "with" : "without") . " memo-fields";
unset($this->headers["checks"], $this->headers["header_length"]);
}

private function readTableHeaders() {
Expand All @@ -184,7 +189,8 @@ private function readTableHeaders() {
"name" => strtolower(trim(substr($data, 0, 11))),
"type" => $data[11],
"length" => unpack("C", $data[16])[1],
"decimal" => unpack("C", $data[17])[1]
"decimal" => unpack("C", $data[17])[1],
"mdx_flag" => unpack("C", $data[31])[1],
];
if ($this->v_foxpro) {
$this->columns[$i]["flag"] = unpack("C", $data[18])[1];
Expand All @@ -201,8 +207,8 @@ private function readTableHeaders() {
$this->columns[$i]["mdx_flag"] = unpack("C", $data[31])[1];
}
}
if ($this->columns[$i] == "C") {
$this->columns[$i]["length"] = unpack("S", substr($data, ($this->dbase7) ? 33 : 16, 2));
if ($this->columns[$i]["type"] == "C") {
$this->columns[$i]["length"] = unpack("S", substr($data, ($this->dbase7) ? 33 : 16, 2))[1];
$this->columns[$i]["decimal"] = 0;
}
}
Expand All @@ -225,19 +231,17 @@ private function getMemoFile($file) {
foreach ($this->memo["formats"] as $format => $versions) {
if (in_array($this->headers["version"], $versions)) {
return $this->fileExists($file.".".$format);
break;
}
}
return false;
}

private function fileExists($fileName) {

if(file_exists($fileName)) {
if (file_exists($fileName)) {
return $fileName;
}

// Handle case insensitive requests
// Handle case-insensitive requests
$directoryName = dirname($fileName);
$fileArray = glob($directoryName . '/*', GLOB_NOSORT);
$fileNameLowerCase = strtolower($fileName);
Expand Down

0 comments on commit d04ce66

Please sign in to comment.