Skip to content

Commit e1e3307

Browse files
committed
Fix BOM handling #586
1 parent 1e7bb18 commit e1e3307

3 files changed

Lines changed: 106 additions & 7 deletions

File tree

src/Reader.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -533,10 +533,9 @@ public function selectAllExcept(string|int ...$columns): TabularDataReader
533533
*/
534534
public function getRecords(array $header = []): Iterator
535535
{
536-
return $this->combineHeader(
537-
$this->prepareRecords(),
538-
$this->prepareHeader($header)
539-
);
536+
$foundHeaders = $this->prepareHeader($header);
537+
538+
return $this->combineHeader($this->prepareRecords(), $foundHeaders);
540539
}
541540

542541
/**
@@ -568,12 +567,12 @@ public function getRecordsAsObject(string $className, array $header = []): Itera
568567
protected function prepareRecords(): Iterator
569568
{
570569
$normalized = fn ($record): bool => is_array($record) && ($this->is_empty_records_included || $record !== [null]);
571-
$bom = null;
570+
$document = $this->getDocument();
572571
if (!$this->is_input_bom_included) {
573-
$bom = Bom::tryFrom($this->getInputBOM());
572+
$document = SkipBomIterator::fromDocument($document, $this->delimiter, $this->enclosure, $this->escape);
574573
}
575574

576-
$records = $this->stripBOM(new CallbackFilterIterator($this->getDocument(), $normalized), $bom);
575+
$records = new CallbackFilterIterator($document, $normalized);
577576
if (null !== $this->header_offset) {
578577
$records = new CallbackFilterIterator($records, fn (array $record, int $offset): bool => $offset !== $this->header_offset);
579578
}
@@ -587,6 +586,7 @@ protected function prepareRecords(): Iterator
587586

588587
/**
589588
* Strips the BOM sequence from the returned records if necessary.
589+
* @deprecated
590590
*/
591591
protected function stripBOM(Iterator $iterator, ?Bom $bom): Iterator
592592
{

src/ReaderTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use function array_keys;
2323
use function count;
24+
use function dump;
2425
use function fclose;
2526
use function fopen;
2627
use function fputcsv;
@@ -652,4 +653,18 @@ public function testStreamWithFiltersDestructsGracefully(): void
652653
// An explicitly closed file handle makes the stream filter resources invalid
653654
fclose($fp);
654655
}
656+
657+
public function test_handline_bom_expression_issue_586(): void
658+
{
659+
$bom = Bom::Utf8->value;
660+
$contents = [
661+
'"a,b",c' => ["a,b", "c"],
662+
'"a'."\n".'b",c' => ['a'."\n".'b', "c"],
663+
];
664+
665+
foreach ($contents as $content => $expected) {
666+
self::assertSame($expected, Reader::fromString($content)->first());
667+
self::assertSame($expected, Reader::fromString($bom.$content)->first());
668+
}
669+
}
655670
}

src/SkipBomIterator.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/**
4+
* League.Csv (https://csv.thephpleague.com)
5+
*
6+
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace League\Csv;
15+
16+
use Iterator;
17+
use SplFileObject;
18+
use const SEEK_SET;
19+
20+
/**
21+
* @internal
22+
*/
23+
final class SkipBomIterator implements Iterator
24+
{
25+
public static function fromDocument(
26+
SplFileObject|Stream $document,
27+
string $delimiter = ',',
28+
string $enclosure = '"',
29+
string $escape = '\\',
30+
): self {
31+
32+
return new self(
33+
$document,
34+
Bom::tryFromSequence($document)?->length() ?? 0,
35+
$delimiter,
36+
$enclosure,
37+
$escape,
38+
);
39+
}
40+
41+
public function __construct(
42+
private readonly SplFileObject|Stream $file,
43+
private readonly int $offset,
44+
private readonly string $delimiter = ',',
45+
private readonly string $enclosure = '"',
46+
private readonly string $escape = '\\',
47+
) {
48+
$this->file->setFlags(SplFileObject::READ_CSV);
49+
$this->file->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
50+
}
51+
52+
public function current(): mixed
53+
{
54+
return $this->file->current();
55+
}
56+
57+
public function fseek(int $offset, int $whence): int
58+
{
59+
return $this->file->fseek($offset, $whence);
60+
}
61+
62+
public function key(): int
63+
{
64+
return $this->file->key();
65+
}
66+
67+
public function next(): void
68+
{
69+
$this->file->next();
70+
}
71+
72+
public function rewind(): void
73+
{
74+
$this->file->rewind();
75+
if (0 !== $this->offset) {
76+
$this->fseek($this->offset, SEEK_SET);
77+
}
78+
}
79+
80+
public function valid(): bool
81+
{
82+
return !$this->file->eof();
83+
}
84+
}

0 commit comments

Comments
 (0)