From b2c49ed399c543d19dc45dd497336200edb89ac5 Mon Sep 17 00:00:00 2001 From: Ondra Date: Wed, 10 Jan 2024 19:08:12 +0100 Subject: [PATCH] CM-873 fix test and implementation --- .gitattributes | 1 + composer.json | 6 +++--- phpcs.xml | 5 ++++- phpstan-baseline-8+.neon | 7 ++++++- phpstan-baseline.neon | 7 ++++++- src/CsvOptions.php | 6 +++--- src/CsvReader.php | 16 ++++++++++----- src/CsvWriter.php | 13 +++++++------ tests/CsvReadTest.php | 23 ++++++++++++++-------- tests/CsvWriteTest.php | 26 ++++++++++++------------- tests/LineBreaksHelperTest.php | 2 +- tests/data/test-input-edgecase.crlf.csv | 1 + 12 files changed, 71 insertions(+), 42 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/data/test-input-edgecase.crlf.csv diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d84a04f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.clrf.csv eol=crlf \ No newline at end of file diff --git a/composer.json b/composer.json index 306ac93..fffe3aa 100644 --- a/composer.json +++ b/composer.json @@ -29,11 +29,11 @@ }, "require-dev": { "ext-json": "*", - "keboola/coding-standard": "^13.0", + "keboola/coding-standard": "^15.0", "php-parallel-lint/php-parallel-lint": "^1.3", - "phpstan/phpstan": "^1.4", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": ">=7.5 <=9.6", - "phpstan/phpdoc-parser": "1.5.*" + "phpstan/phpdoc-parser": "^1.25" }, "scripts": { "phpstan": "phpstan analyse ./src ./tests --level=max --no-progress -c phpstan.neon", diff --git a/phpcs.xml b/phpcs.xml index 3b35890..279710b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -21,4 +21,7 @@ - + + + + \ No newline at end of file diff --git a/phpstan-baseline-8+.neon b/phpstan-baseline-8+.neon index 45a2f3a..2cee76e 100644 --- a/phpstan-baseline-8+.neon +++ b/phpstan-baseline-8+.neon @@ -275,6 +275,11 @@ parameters: count: 1 path: tests/CsvReadTest.php + - + message: "#^Method Keboola\\\\Csv\\\\Tests\\\\CsvReadTest\\:\\:testNewlineDetectionEdgecaseWithCrLf\\(\\) has no return type specified\\.$#" + count: 1 + path: tests/CsvReadTest.php + - message: "#^Method Keboola\\\\Csv\\\\Tests\\\\CsvReadTest\\:\\:testParse\\(\\) has no return type specified\\.$#" count: 1 @@ -493,4 +498,4 @@ parameters: - message: "#^Parameter \\#1 \\$header of static method Keboola\\\\Csv\\\\UTF8BOMHelper\\:\\:detectAndRemoveBOM\\(\\) expects array, mixed given\\.$#" count: 1 - path: tests/UTF8BOMHelperTest.php + path: tests/UTF8BOMHelperTest.php \ No newline at end of file diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a850342..bf9f2b0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -280,6 +280,11 @@ parameters: count: 1 path: tests/CsvReadTest.php + - + message: "#^Method Keboola\\\\Csv\\\\Tests\\\\CsvReadTest\\:\\:testNewlineDetectionEdgecaseWithCrLf\\(\\) has no return type specified\\.$#" + count: 1 + path: tests/CsvReadTest.php + - message: "#^Method Keboola\\\\Csv\\\\Tests\\\\CsvReadTest\\:\\:testParse\\(\\) has no return type specified\\.$#" count: 1 @@ -498,4 +503,4 @@ parameters: - message: "#^Parameter \\#1 \\$header of static method Keboola\\\\Csv\\\\UTF8BOMHelper\\:\\:detectAndRemoveBOM\\(\\) expects array, mixed given\\.$#" count: 1 - path: tests/UTF8BOMHelperTest.php + path: tests/UTF8BOMHelperTest.php \ No newline at end of file diff --git a/src/CsvOptions.php b/src/CsvOptions.php index 4250d0d..167c194 100644 --- a/src/CsvOptions.php +++ b/src/CsvOptions.php @@ -50,7 +50,7 @@ protected function validateEnclosure($enclosure) if (strlen($enclosure) > 1) { throw new InvalidArgumentException( 'Enclosure must be a single character. ' . json_encode($enclosure) . ' received', - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } } @@ -64,14 +64,14 @@ protected function validateDelimiter($delimiter) if (strlen($delimiter) > 1) { throw new InvalidArgumentException( 'Delimiter must be a single character. ' . json_encode($delimiter) . ' received', - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } if (strlen($delimiter) === 0) { throw new InvalidArgumentException( 'Delimiter cannot be empty.', - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } } diff --git a/src/CsvReader.php b/src/CsvReader.php index 40e42ee..e087ed3 100644 --- a/src/CsvReader.php +++ b/src/CsvReader.php @@ -13,6 +13,7 @@ class CsvReader extends AbstractCsvFile implements Iterator * @deprecated use Keboola\Csv\CsvOptions::DEFAULT_ENCLOSURE */ const DEFAULT_ESCAPED_BY = CsvOptions::DEFAULT_ESCAPED_BY; + const SAMPLE_SIZE = 10000; /** * @var int @@ -92,7 +93,7 @@ protected function validateSkipLines($skipLines) if (!is_int($skipLines) || $skipLines < 0) { throw new InvalidArgumentException( "Number of lines to skip must be a positive integer. \"$skipLines\" received.", - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } } @@ -106,14 +107,14 @@ protected function openCsvFile($fileName) if (!is_file($fileName)) { throw new Exception( 'Cannot open file ' . $fileName, - Exception::FILE_NOT_EXISTS + Exception::FILE_NOT_EXISTS, ); } $this->filePointer = @fopen($fileName, 'r'); if (!$this->filePointer) { throw new Exception( "Cannot open file {$fileName} " . error_get_last()['message'], - Exception::FILE_NOT_EXISTS + Exception::FILE_NOT_EXISTS, ); } } @@ -124,7 +125,12 @@ protected function openCsvFile($fileName) protected function detectLineBreak() { @rewind($this->getFilePointer()); - $sample = @fread($this->getFilePointer(), 10000); + $sample = @fread($this->getFilePointer(), self::SAMPLE_SIZE); + if (substr((string) $sample, -1) === "\r") { + // we might have hit the file in the middle of CR+LF, only getting CR + @rewind($this->getFilePointer()); + $sample = @fread($this->getFilePointer(), self::SAMPLE_SIZE+1); + } return LineBreaksHelper::detectLineBreaks($sample, $this->getEnclosure(), $this->getEscapedBy()); } @@ -155,7 +161,7 @@ protected function validateLineBreak() throw new InvalidArgumentException( "Invalid line break. Please use unix \\n or win \\r\\n line breaks.", - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } diff --git a/src/CsvWriter.php b/src/CsvWriter.php index 20e0599..1d17839 100644 --- a/src/CsvWriter.php +++ b/src/CsvWriter.php @@ -55,7 +55,7 @@ private function validateLineBreak($lineBreak) throw new Exception( 'Invalid line break: ' . json_encode($lineBreak) . ' allowed line breaks: ' . json_encode($allowedLineBreaks), - Exception::INVALID_PARAM + Exception::INVALID_PARAM, ); } } @@ -72,13 +72,14 @@ protected function openCsvFile($fileName) throw new Exception( "Cannot open file {$fileName} " . $e->getMessage(), Exception::FILE_NOT_EXISTS, - $e + $e, ); } + if (!$this->filePointer) { throw new Exception( "Cannot open file {$fileName} " . error_get_last()['message'], - Exception::FILE_NOT_EXISTS + Exception::FILE_NOT_EXISTS, ); } } @@ -99,7 +100,7 @@ public function writeRow(array $row) ' Return: false' . ' To write: ' . strlen($str) . ' Written: 0', Exception::WRITE_ERROR, - $e + $e, ); } @@ -113,7 +114,7 @@ public function writeRow(array $row) ($ret === false && error_get_last() ? 'Error: ' . error_get_last()['message'] : '') . ' Return: ' . json_encode($ret) . ' To write: ' . strlen($str) . ' Written: ' . (int) $ret, - Exception::WRITE_ERROR + Exception::WRITE_ERROR, ); } } @@ -137,7 +138,7 @@ public function rowToStr(array $row) )) { throw new Exception( 'Cannot write data into column: ' . var_export($column, true), - Exception::WRITE_ERROR + Exception::WRITE_ERROR, ); } diff --git a/tests/CsvReadTest.php b/tests/CsvReadTest.php index 7700250..9050462 100644 --- a/tests/CsvReadTest.php +++ b/tests/CsvReadTest.php @@ -31,6 +31,13 @@ public function testColumnsCount() self::assertEquals(9, $csv->getColumnsCount()); } + public function testNewlineDetectionEdgecaseWithCrLf() + { + $this->expectNotToPerformAssertions(); + // this used to throw "Invalid line break. Please use unix \n or win \r\n line breaks." before the fix + new CsvReader(__DIR__ . '/data/test-input-edgecase.crlf.csv'); + } + /** * @dataProvider validCsvFiles * @param string $fileName @@ -266,7 +273,7 @@ public function testSkipsHeaders() CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, CsvOptions::DEFAULT_ESCAPED_BY, - 1 + 1, ); self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); self::assertEquals([ @@ -285,7 +292,7 @@ public function testSkipNoLines() CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, CsvOptions::DEFAULT_ESCAPED_BY, - 0 + 0, ); self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); self::assertEquals([ @@ -305,7 +312,7 @@ public function testSkipsMultipleLines() CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, CsvOptions::DEFAULT_ESCAPED_BY, - 3 + 3, ); self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); self::assertEquals([ @@ -322,7 +329,7 @@ public function testSkipsOverflow() CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, CsvOptions::DEFAULT_ESCAPED_BY, - 100 + 100, ); self::assertEquals(['id', 'isImported'], $csvFile->getHeader()); self::assertEquals([], iterator_to_array($csvFile)); @@ -396,7 +403,7 @@ public function testInvalidSkipLines($skipLines, $message) CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, CsvOptions::DEFAULT_ENCLOSURE, - $skipLines + $skipLines, ); } @@ -475,9 +482,9 @@ public function testWriteReadInTheMiddle() '"1","first"', '"2","second"', '', - ] + ], ), - $data + $data, ); } @@ -525,7 +532,7 @@ public function testInvalidFile() public function testPerformance($fileContent, $expectedRows, $maxDuration) { self::markTestSkipped( - 'Run this test only manually. Because the duration is very different in local CI environment.' + 'Run this test only manually. Because the duration is very different in local CI environment.', ); try { diff --git a/tests/CsvWriteTest.php b/tests/CsvWriteTest.php index c35425c..15b54f8 100644 --- a/tests/CsvWriteTest.php +++ b/tests/CsvWriteTest.php @@ -71,9 +71,9 @@ public function testWrite() "\"columns with\nnew line\",\"columns with\ttab\"", '"column with \\n \\t \\\\","second col"', '', - ] + ], ), - $data + $data, ); } @@ -130,9 +130,9 @@ public function testWriteValidObject() '"col1","col2"' , '"1","me string"', '', - ] + ], ), - $data + $data, ); } @@ -191,9 +191,9 @@ public function testWritePointer() [ '"col1","col2"' , 'foo,bar', - ] + ], ), - $data + $data, ); } @@ -213,15 +213,15 @@ public function testInvalidPointer() $or = new LogicalOr(); $or->setConstraints([ new StringContains( - 'Cannot write to CSV file Return: 0 To write: 14 Written: 0' + 'Cannot write to CSV file Return: 0 To write: 14 Written: 0', ), new StringContains( 'Cannot write to CSV file Error: fwrite(): ' . - 'write of 14 bytes failed with errno=9 Bad file descriptor Return: false To write: 14 Written: 0' + 'write of 14 bytes failed with errno=9 Bad file descriptor Return: false To write: 14 Written: 0', ), new StringContains( 'Cannot write to CSV file Error: fwrite(): ' . - 'Write of 14 bytes failed with errno=9 Bad file descriptor Return: false To write: 14 Written: 0' + 'Write of 14 bytes failed with errno=9 Bad file descriptor Return: false To write: 14 Written: 0', ), ]); self::assertThat($e->getMessage(), $or); @@ -238,7 +238,7 @@ public function testInvalidPointer2() $rows = [['col1', 'col2']]; self::expectException(Exception::class); self::expectExceptionMessage( - 'a valid stream resource Return: false To write: 14 Written: ' + 'a valid stream resource Return: false To write: 14 Written: ', ); $csvFile->writeRow($rows[0]); } @@ -258,7 +258,7 @@ public function testWriteLineBreak() $fileName, CsvOptions::DEFAULT_DELIMITER, CsvOptions::DEFAULT_ENCLOSURE, - "\r\n" + "\r\n", ); $rows = [ [ @@ -280,9 +280,9 @@ public function testWriteLineBreak() '"col1","col2"', '"val1","val2"', '', - ] + ], ), - $data + $data, ); } } diff --git a/tests/LineBreaksHelperTest.php b/tests/LineBreaksHelperTest.php index dc47122..b890390 100644 --- a/tests/LineBreaksHelperTest.php +++ b/tests/LineBreaksHelperTest.php @@ -30,7 +30,7 @@ public function testLineBreaksDetection($enclosure, $escapedBy, $input, $expecte // Test line breaks detection self::assertSame( json_encode($expectedLineBreak), - json_encode(LineBreaksHelper::detectLineBreaks($input, $enclosure, $escapedBy)) + json_encode(LineBreaksHelper::detectLineBreaks($input, $enclosure, $escapedBy)), ); } diff --git a/tests/data/test-input-edgecase.crlf.csv b/tests/data/test-input-edgecase.crlf.csv new file mode 100644 index 0000000..afa7c10 --- /dev/null +++ b/tests/data/test-input-edgecase.crlf.csv @@ -0,0 +1 @@ +"1","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"