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