Table::getColumns()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 6
rs 10
1
<?php
2
/********************************************
3
 * DBF-file Structure Reader
4
 *
5
 * Author: Chizhov Nikolay <[email protected]>
6
 * (c) 2019-2024 CIOB "Inok"
7
 ********************************************/
8
9
namespace Inok\Dbf;
10
11
use Exception;
12
13
class Table {
14
  private $headers = null;
15
  private $columns = null;
16
  public $error = false;
17
  public $error_info = null;
18
19
  private $db, $fp;
20
21
  private $versions = [
22
    2   => ["FoxBase"],
23
    3   => ["dBASE III", "dBASE IV", "dBASE 5", "FoxPro", "FoxBASE+"],
24
    4   => ["dBASE 7"],
25
    48  => ["Visual FoxPro"],
26
    49  => ["Visual FoxPro"],
27
    50  => ["Visual FoxPro"],
28
    67  => ["dBASE IV", "dBASE 5"],
29
    99  => ["dBASE IV", "dBASE 5"],
30
    131 => ["dBASE III", "FoxBASE+", "FoxPro"],
31
    139 => ["dBASE IV", "dBASE 5"],
32
    140 => ["dBASE 7"],
33
    203 => ["dBASE IV", "dBASE 5"],
34
    229 => ["SMT"],
35
    235 => ["dBASE IV", "dBASE 5"],
36
    245 => ["FoxPro"],
37
    251 => ["FoxBASE"]
38
  ];
39
  private $memo = [
40
    "versions" => [131, 139, 140, 203, 229, 235, 245, 251],
41
    "formats" => [
42
      "dbt" => [131, 139, 140, 203, 235, 251],
43
      "fpt" => [245, 48, 49, 50],
44
      "smt" => [229]
45
    ]
46
  ];
47
48
  private $charsets = [
49
    0   => 866, //If charset not defined
50
    1   => 437,    2   => 850,    3   => 1252,    4   => 10000,    8   => 865,
51
    9   => 437,    10  => 850,    11  => 437,     13  => 437,      14  => 850,
52
    15  => 437,    16  => 850,    17  => 437,     18  => 850,      19  => 932,
53
    20  => 850,    21  => 850,    22  => 437,     23  => 850,      24  => 437,
54
    25  => 437,    26  => 850,    27  => 437,     28  => 863,      29  => 850,
55
    31  => 852,    34  => 852,    35  => 852,     36  => 860,      37  => 850,
56
    38  => 866,    55  => 850,    64  => 852,     77  => 936,      78  => 949,
57
    79  => 950,    80  => 874,    88  => 1252,    89  => 1252,     100 => 852,
58
    101 => 866,    102 => 865,    103 => 861,     104 => 895,      105 => 866,
59
    106 => 737,    107 => 857,    108 => 863,     120 => 950,      121 => 949,
60
    122 => 936,    123 => 932,    124 => 874,     134 => 737,      135 => 852,
61
    136 => 857,    150 => 10007,  151 => 10029,   152 => 10006,    200 => 1250,
62
    201 => 1251,   202 => 1254,   203 => 1253,    204 => 1257
63
  ];
64
  private $dbase7 = false, $v_foxpro = false;
65
66
  /**
67
   * @throws Exception
68
   */
69
  public function __construct($dbPath, $charset = null){
70
    $this->db = $dbPath;
71
    if (!is_null($charset)) {
72
      if (!is_numeric($charset)) {
73
        throw new Exception("Set not correct charset. Allows only digits.");
74
      }
75
      $this->charsets[0] = $charset;
76
    }
77
    $this->open();
78
  }
79
80
  public function __destruct() {
81
    $this->close();
82
  }
83
84
  /**
85
   * @throws Exception
86
   */
87
  private function open() {
88
    if (!file_exists($this->db)) {
89
      throw new Exception(sprintf('File %s cannot be found', $this->db));
90
    }
91
    $this->fp = fopen($this->db, "rb");
92
  }
93
94
  public function getHeaders() {
95
    if (!$this->error && is_null($this->headers)) {
96
      $this->readHeaders();
97
    }
98
    return $this->headers;
99
  }
100
101
  public function getColumns() {
102
    $this->getHeaders();
103
    if (!$this->error && is_null($this->columns)) {
104
      $this->readTableHeaders();
105
    }
106
    return $this->columns;
107
  }
108
109
  public function getData() {
110
    $this->getColumns();
111
    return $this->fp;
112
  }
113
114
  public function close() {
115
    if (get_resource_type($this->fp) === "file") {
116
      fclose($this->fp);
117
    }
118
  }
119
120
  private function readHeaders() {
121
    $data = fread($this->fp, 32);
122
    $file = pathinfo($this->db);
123
    $this->headers = [
124
      "dbf_file" => $this->db,
125
      "table" => strtolower(basename($file["filename"], ".dbf")),
126
      "version" => unpack("C", $data[0])[1],
127
      "date" => $this->getDate(unpack("C*", substr($data, 1, 3))),
128
      "records" => unpack("L", substr($data, 4, 4))[1],
129
      "header_length" => unpack("S", substr($data, 8, 2))[1],
130
      "record_length" => unpack("S", substr($data, 10, 2))[1],
131
      "unfinish_transaction" => unpack("C", $data[14])[1],
132
      "coded" => unpack("C", $data[15])[1],
133
      "mdx_flag" => unpack("C", $data[28])[1],
134
      "charset" => unpack("C", $data[29])[1],
135
      "checks" => [
136
        unpack("S", substr($data, 12, 2))[1], unpack("S", substr($data, 30, 2))[1]
137
      ]
138
    ];
139
140
    if ($this->headers["checks"][0] != 0) {
141
      $this->error = true;
142
      $this->error_info = "Not correct DBF file by headers";
143
      return;
144
    }
145
    $this->headers["charset_name"] = "cp" . $this->charsets[$this->headers["charset"]];
146
147
    if (in_array("dBASE 7", $this->versions[$this->headers["version"]])) {
148
      $this->dbase7 = true;
149
      $this->headers["columns"] = ($this->headers["header_length"] - 68) / 48;
150
    } elseif (in_array("Visual FoxPro", $this->versions[$this->headers["version"]])) {
151
      $this->v_foxpro = true;
152
      $this->headers["memo"] = (in_array($this->headers["mdx_flag"], [2, 3, 6, 7]));
153
      $this->headers["columns"] = ($this->headers["header_length"] - 296) / 32;
154
    } else {
155
      $this->headers["columns"] = ($this->headers["header_length"] - 33) / 32;
156
    }
157
158
    if (!isset($this->headers["memo"])) {
159
      $this->headers["memo"] = in_array($this->headers["version"], $this->memo["versions"]);
160
    }
161
    if ($this->headers["memo"]) {
162
      $this->headers["memo_file"] = ($mfile = $this->getMemoFile($file["dirname"] . "/" . $file["filename"])) ? $mfile : null;
163
    }
164
165
    $this->headers["version_name"] =
166
      implode(", ", $this->versions[$this->headers["version"]]) . " " . ($this->headers["memo"] ? "with" : "without") . " memo-fields";
167
    unset($this->headers["checks"], $this->headers["header_length"]);
168
  }
169
170
  private function readTableHeaders() {
171
    if (!$this->error && is_null($this->headers)) {
172
      $this->readHeaders();
173
    }
174
    if (!$this->error) {
175
      for ($i = 0; $i < $this->headers["columns"]; $i++) {
176
        $data = fread($this->fp, ($this->dbase7) ? 48 : 32);
177
        if ($this->dbase7) {
178
          $this->columns[$i] = [
179
            "name" => strtolower(trim(substr($data, 0, 32))),
180
            "type" => $data[32],
181
            "length" => unpack("C", $data[33])[1],
182
            "decimal" => unpack("C", $data[34])[1],
183
            "mdx_flag" => unpack("C", $data[37])[1],
184
            "auto_increment" => unpack("L", substr($data, 40, 4))[1]
185
          ];
186
        }
187
        else {
188
          $this->columns[$i] = [
189
            "name" => strtolower(trim(substr($data, 0, 11))),
190
            "type" => $data[11],
191
            "length" => unpack("C", $data[16])[1],
192
            "decimal" => unpack("C", $data[17])[1],
193
            "mdx_flag" => unpack("C", $data[31])[1],
194
          ];
195
          if ($this->v_foxpro) {
196
            $this->columns[$i]["flag"] = unpack("C", $data[18])[1];
197
            $this->columns[$i]["system"] = ($this->columns[$i]["flag"] == 1);
198
            $this->columns[$i]["has_null"] = in_array($this->columns[$i]["flag"], [2, 6]);
199
            $this->columns[$i]["binary"] = in_array($this->columns[$i]["flag"], [4, 6]);
200
            $this->columns[$i]["auto_increment"] = ($this->columns[$i]["flag"] == 12);
201
            if ($this->columns[$i]["auto_increment"]) {
202
              $this->columns[$i]["auto_increment_next"] = unpack("L", substr($data, 19, 4))[1];
203
              $this->columns[$i]["auto_increment_step"] = unpack("C", $data[23])[1];
204
            }
205
          }
206
          else {
207
            $this->columns[$i]["mdx_flag"] = unpack("C", $data[31])[1];
208
          }
209
        }
210
        if ($this->columns[$i]["type"] == "C") {
211
          $this->columns[$i]["length"] = unpack("S", substr($data, ($this->dbase7) ? 33 : 16, 2))[1];
212
          $this->columns[$i]["decimal"] = 0;
213
        }
214
      }
215
    }
216
    $terminal_byte = unpack("C", fread($this->fp, 1))[1];
217
    if ($terminal_byte != 13) {
218
      $this->error = true;
219
      $this->error_info = "Not correct DBF file by columns";
220
    }
221
    if ($this->v_foxpro) {
222
      fread($this->fp, 263);
223
    }
224
  }
225
226
  private function getDate($data) {
227
    return $data[3].".".$data[2].".".($data[1] > 70 ? 1900 + $data[1] : 2000 + $data[1]);
228
  }
229
230
  private function getMemoFile($file) {
231
    foreach ($this->memo["formats"] as $format => $versions) {
232
      if (in_array($this->headers["version"], $versions)) {
233
        return $this->fileExists($file.".".$format);
234
      }
235
    }
236
    return false;
237
  }
238
239
  private function fileExists($fileName) {
240
    if (file_exists($fileName)) {
241
      return $fileName;
242
    }
243
244
    // Handle case-insensitive requests
245
    $directoryName = dirname($fileName);
246
    $fileArray = glob($directoryName . '/*', GLOB_NOSORT);
247
    $fileNameLowerCase = strtolower($fileName);
248
    foreach($fileArray as $file) {
249
      if(strtolower($file) == $fileNameLowerCase) {
250
        return $file;
251
      }
252
    }
253
    return false;
254
  }
255
}
256