Failed Conditions
Push — master ( d6a367...8799a0 )
by
unknown
16:08 queued 05:56
created

Csv::getCsv()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 4
c 2
b 0
f 0
dl 0
loc 13
ccs 4
cts 5
cp 0.8
rs 10
cc 3
nc 2
nop 5
crap 3.072
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter;
9
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
13
use Throwable;
14
15
class Csv extends BaseReader
16
{
17
    const DEFAULT_FALLBACK_ENCODING = 'CP1252';
18
    const GUESS_ENCODING = 'guess';
19
    const UTF8_BOM = "\xEF\xBB\xBF";
20
    const UTF8_BOM_LEN = 3;
21
    const UTF16BE_BOM = "\xfe\xff";
22
    const UTF16BE_BOM_LEN = 2;
23
    const UTF16BE_LF = "\x00\x0a";
24
    const UTF16LE_BOM = "\xff\xfe";
25
    const UTF16LE_BOM_LEN = 2;
26
    const UTF16LE_LF = "\x0a\x00";
27
    const UTF32BE_BOM = "\x00\x00\xfe\xff";
28
    const UTF32BE_BOM_LEN = 4;
29
    const UTF32BE_LF = "\x00\x00\x00\x0a";
30
    const UTF32LE_BOM = "\xff\xfe\x00\x00";
31
    const UTF32LE_BOM_LEN = 4;
32
    const UTF32LE_LF = "\x0a\x00\x00\x00";
33
34
    /**
35
     * Input encoding.
36
     */
37
    private string $inputEncoding = 'UTF-8';
38
39
    /**
40
     * Fallback encoding if guess strikes out.
41
     */
42
    private string $fallbackEncoding = self::DEFAULT_FALLBACK_ENCODING;
43
44
    /**
45
     * Delimiter.
46
     */
47
    private ?string $delimiter = null;
48
49
    /**
50
     * Enclosure.
51
     */
52
    private string $enclosure = '"';
53
54
    /**
55
     * Sheet index to read.
56
     */
57
    private int $sheetIndex = 0;
58
59
    /**
60
     * Load rows contiguously.
61
     */
62
    private bool $contiguous = false;
63
64
    /**
65
     * The character that can escape the enclosure.
66
     * This will probably become unsupported in Php 9.
67
     * Not yet ready to mark deprecated in order to give users
68
     * a migration path.
69
     */
70
    private ?string $escapeCharacter = null;
71
72
    /**
73
     * The character that will be supplied to fgetcsv
74
     * when escapeCharacter is null.
75
     * It is anticipated that it will conditionally be set
76
     * to null-string for Php9 and above.
77
     */
78
    private static string $defaultEscapeCharacter = PHP_VERSION_ID < 90000 ? '\\' : '';
79
80
    /**
81
     * Callback for setting defaults in construction.
82
     *
83
     * @var ?callable
84
     */
85
    private static $constructorCallback;
86
87
    /**
88
     * Attempt autodetect line endings (deprecated after PHP8.1)?
89
     */
90
    private bool $testAutodetect = true;
91
92
    protected bool $castFormattedNumberToNumeric = false;
93
94
    protected bool $preserveNumericFormatting = false;
95
96
    private bool $preserveNullString = false;
97
98
    private bool $sheetNameIsFileName = false;
99
100
    private string $getTrue = 'true';
101
102
    private string $getFalse = 'false';
103
104
    private string $thousandsSeparator = ',';
105
106
    private string $decimalSeparator = '.';
107
108
    /**
109
     * Create a new CSV Reader instance.
110
     */
111 147
    public function __construct()
112
    {
113 147
        parent::__construct();
114 147
        $callback = self::$constructorCallback;
115 147
        if ($callback !== null) {
116 5
            $callback($this);
117
        }
118
    }
119
120
    /**
121
     * Set a callback to change the defaults.
122
     *
123
     * The callback must accept the Csv Reader object as the first parameter,
124
     * and it should return void.
125
     */
126 6
    public static function setConstructorCallback(?callable $callback): void
127
    {
128 6
        self::$constructorCallback = $callback;
129
    }
130
131 1
    public static function getConstructorCallback(): ?callable
132
    {
133 1
        return self::$constructorCallback;
134
    }
135
136 47
    public function setInputEncoding(string $encoding): self
137
    {
138 47
        $this->inputEncoding = $encoding;
139
140 47
        return $this;
141
    }
142
143 1
    public function getInputEncoding(): string
144
    {
145 1
        return $this->inputEncoding;
146
    }
147
148 5
    public function setFallbackEncoding(string $fallbackEncoding): self
149
    {
150 5
        $this->fallbackEncoding = $fallbackEncoding;
151
152 5
        return $this;
153
    }
154
155 1
    public function getFallbackEncoding(): string
156
    {
157 1
        return $this->fallbackEncoding;
158
    }
159
160
    /**
161
     * Move filepointer past any BOM marker.
162
     */
163 125
    protected function skipBOM(): void
164
    {
165 125
        rewind($this->fileHandle);
166
167 125
        if (fgets($this->fileHandle, self::UTF8_BOM_LEN + 1) !== self::UTF8_BOM) {
168 110
            rewind($this->fileHandle);
169
        }
170
    }
171
172
    /**
173
     * Identify any separator that is explicitly set in the file.
174
     */
175 125
    protected function checkSeparator(): void
176
    {
177 125
        $line = fgets($this->fileHandle);
178 125
        if ($line === false) {
179 2
            return;
180
        }
181
182 124
        if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) {
183 3
            $this->delimiter = substr($line, 4, 1);
184
185 3
            return;
186
        }
187
188 122
        $this->skipBOM();
189
    }
190
191
    /**
192
     * Infer the separator if it isn't explicitly set in the file or specified by the user.
193
     */
194 125
    protected function inferSeparator(): void
195
    {
196 125
        if ($this->delimiter !== null) {
197 23
            return;
198
        }
199
200 114
        $inferenceEngine = new Delimiter($this->fileHandle, $this->escapeCharacter ?? self::$defaultEscapeCharacter, $this->enclosure);
201
202
        // If number of lines is 0, nothing to infer : fall back to the default
203 114
        if ($inferenceEngine->linesCounted() === 0) {
204 2
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
205 2
            $this->skipBOM();
206
207 2
            return;
208
        }
209
210 113
        $this->delimiter = $inferenceEngine->infer();
211
212
        // If no delimiter could be detected, fall back to the default
213 113
        if ($this->delimiter === null) {
214 10
            $this->delimiter = $inferenceEngine->getDefaultDelimiter();
215
        }
216
217 113
        $this->skipBOM();
218
    }
219
220
    /**
221
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
222
     */
223 12
    public function listWorksheetInfo(string $filename): array
224
    {
225
        // Open file
226 12
        $this->openFileOrMemory($filename);
227 11
        $fileHandle = $this->fileHandle;
228
229
        // Skip BOM, if any
230 11
        $this->skipBOM();
231 11
        $this->checkSeparator();
232 11
        $this->inferSeparator();
233
234 11
        $worksheetInfo = [];
235 11
        $worksheetInfo[0]['worksheetName'] = 'Worksheet';
236 11
        $worksheetInfo[0]['lastColumnLetter'] = 'A';
237 11
        $worksheetInfo[0]['lastColumnIndex'] = 0;
238 11
        $worksheetInfo[0]['totalRows'] = 0;
239 11
        $worksheetInfo[0]['totalColumns'] = 0;
240 11
        $delimiter = $this->delimiter ?? '';
241
242
        // Loop through each line of the file in turn
243 11
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
244 11
        while (is_array($rowData)) {
245 11
            ++$worksheetInfo[0]['totalRows'];
246 11
            $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], count($rowData) - 1);
247 11
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
248
        }
249
250 11
        $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
251 11
        $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
252
253
        // Close file
254 11
        fclose($fileHandle);
255
256 11
        return $worksheetInfo;
257
    }
258
259
    /**
260
     * Loads Spreadsheet from file.
261
     */
262 110
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
263
    {
264
        // Create new Spreadsheet
265 110
        $spreadsheet = new Spreadsheet();
266 110
        $spreadsheet->setValueBinder($this->valueBinder);
267
268
        // Load into this instance
269 110
        return $this->loadIntoExisting($filename, $spreadsheet);
270
    }
271
272
    /**
273
     * Loads Spreadsheet from string.
274
     */
275 4
    public function loadSpreadsheetFromString(string $contents): Spreadsheet
276
    {
277
        // Create new Spreadsheet
278 4
        $spreadsheet = new Spreadsheet();
279 4
        $spreadsheet->setValueBinder($this->valueBinder);
280
281
        // Load into this instance
282 4
        return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
283
    }
284
285 124
    private function openFileOrMemory(string $filename): void
286
    {
287
        // Open file
288 124
        $fhandle = $this->canRead($filename);
289 124
        if (!$fhandle) {
290 3
            throw new ReaderException($filename . ' is an Invalid Spreadsheet file.');
291
        }
292 121
        if ($this->inputEncoding === 'UTF-8') {
293 81
            $encoding = self::guessEncodingBom($filename);
294 81
            if ($encoding !== '') {
295 6
                $this->inputEncoding = $encoding;
296
            }
297
        }
298 121
        if ($this->inputEncoding === self::GUESS_ENCODING) {
299 18
            $this->inputEncoding = self::guessEncoding($filename, $this->fallbackEncoding);
300
        }
301 121
        $this->openFile($filename);
302 121
        if ($this->inputEncoding !== 'UTF-8') {
303 38
            fclose($this->fileHandle);
304 38
            $entireFile = file_get_contents($filename);
305 38
            $fileHandle = fopen('php://memory', 'r+b');
306 38
            if ($fileHandle !== false && $entireFile !== false) {
307 38
                $this->fileHandle = $fileHandle;
308 38
                $data = StringHelper::convertEncoding($entireFile, 'UTF-8', $this->inputEncoding);
309 38
                fwrite($this->fileHandle, $data);
310 38
                $this->skipBOM();
311
            }
312
        }
313
    }
314
315 4
    public function setTestAutoDetect(bool $value): self
316
    {
317 4
        $this->testAutodetect = $value;
318
319 4
        return $this;
320
    }
321
322 117
    private function setAutoDetect(?string $value): ?string
323
    {
324 117
        $retVal = null;
325 117
        if ($value !== null && $this->testAutodetect && PHP_VERSION_ID < 90000) {
326 114
            $retVal2 = @ini_set('auto_detect_line_endings', $value);
327 114
            if (is_string($retVal2)) {
328 114
                $retVal = $retVal2;
329
            }
330
        }
331
332 117
        return $retVal;
333
    }
334
335 14
    public function castFormattedNumberToNumeric(
336
        bool $castFormattedNumberToNumeric,
337
        bool $preserveNumericFormatting = false
338
    ): void {
339 14
        $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric;
340 14
        $this->preserveNumericFormatting = $preserveNumericFormatting;
341
    }
342
343
    /**
344
     * Open data uri for reading.
345
     */
346 4
    private function openDataUri(string $filename): void
347
    {
348 4
        $fileHandle = fopen($filename, 'rb');
349 4
        if ($fileHandle === false) {
350
            // @codeCoverageIgnoreStart
351
            throw new ReaderException('Could not open file ' . $filename . ' for reading.');
352
            // @codeCoverageIgnoreEnd
353
        }
354
355 4
        $this->fileHandle = $fileHandle;
356
    }
357
358
    /**
359
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
360
     */
361 113
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
362
    {
363 113
        return $this->loadStringOrFile($filename, $spreadsheet, false);
364
    }
365
366
    /**
367
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
368
     */
369 117
    private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet
370
    {
371
        // Deprecated in Php8.1
372 117
        $iniset = $this->setAutoDetect('1');
373
374
        try {
375 117
            $this->loadStringOrFile2($filename, $spreadsheet, $dataUri);
376 115
            $this->setAutoDetect($iniset);
377 2
        } catch (Throwable $e) {
378 2
            $this->setAutoDetect($iniset);
379
380 2
            throw $e;
381
        }
382
383 115
        return $spreadsheet;
384
    }
385
386 117
    private function loadStringOrFile2(string $filename, Spreadsheet $spreadsheet, bool $dataUri): void
387
    {
388
389
        // Open file
390 117
        if ($dataUri) {
391 4
            $this->openDataUri($filename);
392
        } else {
393 113
            $this->openFileOrMemory($filename);
394
        }
395 115
        $fileHandle = $this->fileHandle;
396
397
        // Skip BOM, if any
398 115
        $this->skipBOM();
399 115
        $this->checkSeparator();
400 115
        $this->inferSeparator();
401
402
        // Create new PhpSpreadsheet object
403 115
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
404 4
            $spreadsheet->createSheet();
405
        }
406 115
        $sheet = $spreadsheet->setActiveSheetIndex($this->sheetIndex);
407 115
        if ($this->sheetNameIsFileName) {
408 4
            $sheet->setTitle(substr(basename($filename, '.csv'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
409
        }
410
411
        // Set our starting row based on whether we're in contiguous mode or not
412 115
        $currentRow = 1;
413 115
        $outRow = 0;
414
415
        // Loop through each line of the file in turn
416 115
        $delimiter = $this->delimiter ?? '';
417 115
        $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
418 115
        $valueBinder = $this->valueBinder ?? Cell::getValueBinder();
419 115
        $preserveBooleanString = method_exists($valueBinder, 'getBooleanConversion') && $valueBinder->getBooleanConversion();
420 115
        $this->getTrue = Calculation::getTRUE();
421 115
        $this->getFalse = Calculation::getFALSE();
422 115
        $this->thousandsSeparator = StringHelper::getThousandsSeparator();
423 115
        $this->decimalSeparator = StringHelper::getDecimalSeparator();
424 115
        while (is_array($rowData)) {
425 114
            $noOutputYet = true;
426 114
            $columnLetter = 'A';
427 114
            foreach ($rowData as $rowDatum) {
428 114
                if ($preserveBooleanString) {
429 5
                    $rowDatum = $rowDatum ?? '';
430
                } else {
431 110
                    $this->convertBoolean($rowDatum);
432
                }
433 114
                $numberFormatMask = $this->castFormattedNumberToNumeric ? $this->convertFormattedNumber($rowDatum) : '';
434 114
                if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) {
435 114
                    if ($this->contiguous) {
436 3
                        if ($noOutputYet) {
437 3
                            $noOutputYet = false;
438 3
                            ++$outRow;
439
                        }
440
                    } else {
441 111
                        $outRow = $currentRow;
442
                    }
443
                    // Set basic styling for the value (Note that this could be overloaded by styling in a value binder)
444 114
                    if ($numberFormatMask !== '') {
445 7
                        $sheet->getStyle($columnLetter . $outRow)
446 7
                            ->getNumberFormat()
447 7
                            ->setFormatCode($numberFormatMask);
448
                    }
449
                    // Set cell value
450 114
                    $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum);
451
                }
452 114
                ++$columnLetter;
453
            }
454 114
            $rowData = self::getCsv($fileHandle, 0, $delimiter, $this->enclosure, $this->escapeCharacter);
455 114
            ++$currentRow;
456
        }
457
458
        // Close file
459 115
        fclose($fileHandle);
460
    }
461
462
    /**
463
     * Convert string true/false to boolean, and null to null-string.
464
     */
465 110
    private function convertBoolean(mixed &$rowDatum): void
466
    {
467 110
        if (is_string($rowDatum)) {
468 110
            if (strcasecmp($this->getTrue, $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) {
469 12
                $rowDatum = true;
470 110
            } elseif (strcasecmp($this->getFalse, $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
471 12
                $rowDatum = false;
472
            }
473
        } else {
474
            $rowDatum = $rowDatum ?? '';
475
        }
476
    }
477
478
    /**
479
     * Convert numeric strings to int or float values.
480
     */
481 14
    private function convertFormattedNumber(mixed &$rowDatum): string
482
    {
483 14
        $numberFormatMask = '';
484 14
        if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) {
485 14
            $numeric = str_replace(
486 14
                [$this->thousandsSeparator, $this->decimalSeparator],
487 14
                ['', '.'],
488 14
                $rowDatum
489 14
            );
490
491 14
            if (is_numeric($numeric)) {
492 14
                $decimalPos = strpos($rowDatum, $this->decimalSeparator);
493 14
                if ($this->preserveNumericFormatting === true) {
494 7
                    $numberFormatMask = (str_contains($rowDatum, $this->thousandsSeparator))
495 7
                        ? '#,##0' : '0';
496 7
                    if ($decimalPos !== false) {
497 7
                        $decimals = strlen($rowDatum) - $decimalPos - 1;
498 7
                        $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6));
499
                    }
500
                }
501
502 14
                $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric;
503
            }
504
        }
505
506 14
        return $numberFormatMask;
507
    }
508
509 14
    public function getDelimiter(): ?string
510
    {
511 14
        return $this->delimiter;
512
    }
513
514 11
    public function setDelimiter(?string $delimiter): self
515
    {
516 11
        $this->delimiter = $delimiter;
517
518 11
        return $this;
519
    }
520
521 2
    public function getEnclosure(): string
522
    {
523 2
        return $this->enclosure;
524
    }
525
526 10
    public function setEnclosure(string $enclosure): self
527
    {
528 10
        if ($enclosure == '') {
529 3
            $enclosure = '"';
530
        }
531 10
        $this->enclosure = $enclosure;
532
533 10
        return $this;
534
    }
535
536 1
    public function getSheetIndex(): int
537
    {
538 1
        return $this->sheetIndex;
539
    }
540
541 5
    public function setSheetIndex(int $indexValue): self
542
    {
543 5
        $this->sheetIndex = $indexValue;
544
545 5
        return $this;
546
    }
547
548 3
    public function setContiguous(bool $contiguous): self
549
    {
550 3
        $this->contiguous = $contiguous;
551
552 3
        return $this;
553
    }
554
555 1
    public function getContiguous(): bool
556
    {
557 1
        return $this->contiguous;
558
    }
559
560
    /**
561
     * Php9 intends to drop support for this parameter in fgetcsv.
562
     * Not yet ready to mark deprecated in order to give users
563
     * a migration path.
564
     */
565 9
    public function setEscapeCharacter(string $escapeCharacter): self
566
    {
567 9
        if (PHP_VERSION_ID >= 90000 && $escapeCharacter !== '') {
568
            throw new ReaderException('Escape character must be null string for Php9+');
569
        }
570
571 9
        $this->escapeCharacter = $escapeCharacter;
572
573 9
        return $this;
574
    }
575
576 1
    public function getEscapeCharacter(): string
577
    {
578 1
        return $this->escapeCharacter ?? self::$defaultEscapeCharacter;
579
    }
580
581
    /**
582
     * Can the current IReader read the file?
583
     */
584 140
    public function canRead(string $filename): bool
585
    {
586
        // Check if file exists
587
        try {
588 140
            $this->openFile($filename);
589 3
        } catch (ReaderException) {
590 3
            return false;
591
        }
592
593 137
        fclose($this->fileHandle);
594
595
        // Trust file extension if any
596 137
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
597 137
        if (in_array($extension, ['csv', 'tsv'])) {
598 106
            return true;
599
        }
600
601
        // Attempt to guess mimetype
602 32
        $type = mime_content_type($filename);
603 32
        $supportedTypes = [
604 32
            'application/csv',
605 32
            'text/csv',
606 32
            'text/plain',
607 32
            'inode/x-empty',
608 32
            'text/html',
609 32
        ];
610
611 32
        return in_array($type, $supportedTypes, true);
612
    }
613
614 21
    private static function guessEncodingTestNoBom(string &$encoding, string &$contents, string $compare, string $setEncoding): void
615
    {
616 21
        if ($encoding === '') {
617 21
            $pos = strpos($contents, $compare);
618 21
            if ($pos !== false && $pos % strlen($compare) === 0) {
619 11
                $encoding = $setEncoding;
620
            }
621
        }
622
    }
623
624 21
    private static function guessEncodingNoBom(string $filename): string
625
    {
626 21
        $encoding = '';
627 21
        $contents = file_get_contents($filename);
628 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32BE_LF, 'UTF-32BE');
629 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF32LE_LF, 'UTF-32LE');
630 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16BE_LF, 'UTF-16BE');
631 21
        self::guessEncodingTestNoBom($encoding, $contents, self::UTF16LE_LF, 'UTF-16LE');
632 21
        if ($encoding === '' && preg_match('//u', $contents) === 1) {
633 4
            $encoding = 'UTF-8';
634
        }
635
636 21
        return $encoding;
637
    }
638
639 109
    private static function guessEncodingTestBom(string &$encoding, string $first4, string $compare, string $setEncoding): void
640
    {
641 109
        if ($encoding === '') {
642 109
            if (str_starts_with($first4, $compare)) {
643 15
                $encoding = $setEncoding;
644
            }
645
        }
646
    }
647
648 109
    public static function guessEncodingBom(string $filename, ?string $convertString = null): string
649
    {
650 109
        $encoding = '';
651 109
        $first4 = $convertString ?? (string) file_get_contents($filename, false, null, 0, 4);
652 109
        self::guessEncodingTestBom($encoding, $first4, self::UTF8_BOM, 'UTF-8');
653 109
        self::guessEncodingTestBom($encoding, $first4, self::UTF16BE_BOM, 'UTF-16BE');
654 109
        self::guessEncodingTestBom($encoding, $first4, self::UTF32BE_BOM, 'UTF-32BE');
655 109
        self::guessEncodingTestBom($encoding, $first4, self::UTF32LE_BOM, 'UTF-32LE');
656 109
        self::guessEncodingTestBom($encoding, $first4, self::UTF16LE_BOM, 'UTF-16LE');
657
658 109
        return $encoding;
659
    }
660
661 31
    public static function guessEncoding(string $filename, string $dflt = self::DEFAULT_FALLBACK_ENCODING): string
662
    {
663 31
        $encoding = self::guessEncodingBom($filename);
664 31
        if ($encoding === '') {
665 21
            $encoding = self::guessEncodingNoBom($filename);
666
        }
667
668 31
        return ($encoding === '') ? $dflt : $encoding;
669
    }
670
671 1
    public function setPreserveNullString(bool $value): self
672
    {
673 1
        $this->preserveNullString = $value;
674
675 1
        return $this;
676
    }
677
678 1
    public function getPreserveNullString(): bool
679
    {
680 1
        return $this->preserveNullString;
681
    }
682
683 4
    public function setSheetNameIsFileName(bool $sheetNameIsFileName): self
684
    {
685 4
        $this->sheetNameIsFileName = $sheetNameIsFileName;
686
687 4
        return $this;
688
    }
689
690
    /**
691
     * Php8.4 deprecates use of anything other than null string
692
     * as escape Character.
693
     *
694
     * @param resource $stream
695
     * @param null|int<0, max> $length
696
     *
697
     * @return array<int,?string>|false
698
     */
699 125
    private static function getCsv(
700
        $stream,
701
        ?int $length = null,
702
        string $separator = ',',
703
        string $enclosure = '"',
704
        ?string $escape = null
705
    ): array|false {
706 125
        $escape = $escape ?? self::$defaultEscapeCharacter;
707 125
        if (PHP_VERSION_ID >= 80400 && $escape !== '') {
708
            return @fgetcsv($stream, $length, $separator, $enclosure, $escape);
709
        }
710
711 125
        return fgetcsv($stream, $length, $separator, $enclosure, $escape);
712
    }
713
714 1
    public static function affectedByPhp9(
715
        string $filename,
716
        string $inputEncoding = 'UTF-8',
717
        ?string $delimiter = null,
718
        string $enclosure = '"',
719
        string $escapeCharacter = '\\'
720
    ): bool {
721 1
        if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 90000) {
722
            throw new ReaderException('Function valid only for Php7.4 or Php8'); // @codeCoverageIgnore
723
        }
724 1
        $reader1 = new self();
725 1
        $reader1->setInputEncoding($inputEncoding)
726 1
            ->setTestAutoDetect(true)
727 1
            ->setEscapeCharacter($escapeCharacter)
728 1
            ->setDelimiter($delimiter)
729 1
            ->setEnclosure($enclosure);
730 1
        $spreadsheet1 = $reader1->load($filename);
731 1
        $sheet1 = $spreadsheet1->getActiveSheet();
732 1
        $array1 = $sheet1->toArray(null, false, false);
733 1
        $spreadsheet1->disconnectWorksheets();
734
735 1
        $reader2 = new self();
736 1
        $reader2->setInputEncoding($inputEncoding)
737 1
            ->setTestAutoDetect(false)
738 1
            ->setEscapeCharacter('')
739 1
            ->setDelimiter($delimiter)
740 1
            ->setEnclosure($enclosure);
741 1
        $spreadsheet2 = $reader2->load($filename);
742 1
        $sheet2 = $spreadsheet2->getActiveSheet();
743 1
        $array2 = $sheet2->toArray(null, false, false);
744 1
        $spreadsheet2->disconnectWorksheets();
745
746 1
        return $array1 !== $array2;
747
    }
748
}
749