|
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
|
|
|
|