From e4154dfdc3f99a8f41598db16166f0563fcdc799 Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Fri, 28 Aug 2015 23:15:42 -0700 Subject: [PATCH] ODS Reader Spout can now read ODS files. It's on par with the XLSX reader. The only difference is that the row iterator cannot be rewound. It supports the different output formats from LibreOffice and Excel, skipping extra rows/cells if needed. --- .../IteratorNotRewindableException.php | 12 + src/Spout/Reader/ODS/Reader.php | 62 +++ src/Spout/Reader/ODS/RowIterator.php | 314 +++++++++++++++ src/Spout/Reader/ODS/Sheet.php | 63 +++ src/Spout/Reader/ODS/SheetIterator.php | 135 +++++++ src/Spout/Reader/ReaderFactory.php | 3 + src/Spout/Reader/Wrapper/XMLReader.php | 18 + .../XLSX/Helper/SharedStringsHelper.php | 2 +- src/Spout/Reader/XLSX/Helper/SheetHelper.php | 4 +- src/Spout/Reader/XLSX/RowIterator.php | 85 ++-- src/Spout/Reader/XLSX/Sheet.php | 15 +- tests/Spout/Reader/ODS/ReaderTest.php | 371 ++++++++++++++++++ tests/Spout/Reader/XLSX/ReaderTest.php | 3 +- tests/Spout/Reader/XLSX/SheetTest.php | 2 - tests/Spout/Writer/ODS/WriterTest.php | 1 - tests/resources/ods/attack_billion_laughs.ods | Bin 0 -> 2681 bytes .../resources/ods/attack_quadratic_blowup.ods | Bin 0 -> 2682 bytes tests/resources/ods/file_corrupted.ods | Bin 0 -> 1735 bytes .../file_generated_by_excel_2010_windows.ods | Bin 0 -> 3281 bytes .../file_generated_by_excel_office_online.ods | Bin 0 -> 3054 bytes .../ods/file_generated_by_libre_office.ods | Bin 0 -> 9833 bytes .../resources/ods/one_sheet_with_strings.ods | Bin 0 -> 2561 bytes .../ods/sheet_with_all_cell_types.ods | Bin 0 -> 2594 bytes tests/resources/ods/sheet_with_empty_row.ods | Bin 0 -> 2571 bytes tests/resources/ods/sheet_with_formulas.ods | Bin 0 -> 8450 bytes .../ods/sheet_with_multiline_string.ods | Bin 0 -> 2549 bytes tests/resources/ods/sheet_with_no_cells.ods | Bin 0 -> 6799 bytes .../sheet_with_number_columns_repeated.ods | Bin 0 -> 2592 bytes .../ods/sheet_with_only_one_cell.ods | Bin 0 -> 2531 bytes .../ods/sheet_with_undefined_value_type.ods | Bin 0 -> 2552 bytes .../ods/sheet_with_various_spaces.ods | Bin 0 -> 2833 bytes .../resources/ods/two_sheets_with_strings.ods | Bin 0 -> 2633 bytes .../resources/xlsx/attack_billion_laughs.xlsx | Bin 4051 -> 3911 bytes .../xlsx/attack_quadratic_blowup.xlsx | Bin 4017 -> 3877 bytes ...ty_rows.xlsx => sheet_with_empty_row.xlsx} | Bin 35 files changed, 1025 insertions(+), 65 deletions(-) create mode 100644 src/Spout/Reader/Exception/IteratorNotRewindableException.php create mode 100644 src/Spout/Reader/ODS/Reader.php create mode 100644 src/Spout/Reader/ODS/RowIterator.php create mode 100644 src/Spout/Reader/ODS/Sheet.php create mode 100644 src/Spout/Reader/ODS/SheetIterator.php create mode 100644 tests/Spout/Reader/ODS/ReaderTest.php create mode 100644 tests/resources/ods/attack_billion_laughs.ods create mode 100644 tests/resources/ods/attack_quadratic_blowup.ods create mode 100644 tests/resources/ods/file_corrupted.ods create mode 100755 tests/resources/ods/file_generated_by_excel_2010_windows.ods create mode 100644 tests/resources/ods/file_generated_by_excel_office_online.ods create mode 100644 tests/resources/ods/file_generated_by_libre_office.ods create mode 100644 tests/resources/ods/one_sheet_with_strings.ods create mode 100644 tests/resources/ods/sheet_with_all_cell_types.ods create mode 100644 tests/resources/ods/sheet_with_empty_row.ods create mode 100644 tests/resources/ods/sheet_with_formulas.ods create mode 100644 tests/resources/ods/sheet_with_multiline_string.ods create mode 100644 tests/resources/ods/sheet_with_no_cells.ods create mode 100644 tests/resources/ods/sheet_with_number_columns_repeated.ods create mode 100644 tests/resources/ods/sheet_with_only_one_cell.ods create mode 100644 tests/resources/ods/sheet_with_undefined_value_type.ods create mode 100644 tests/resources/ods/sheet_with_various_spaces.ods create mode 100644 tests/resources/ods/two_sheets_with_strings.ods rename tests/resources/xlsx/{sheet_with_empty_rows.xlsx => sheet_with_empty_row.xlsx} (100%) diff --git a/src/Spout/Reader/Exception/IteratorNotRewindableException.php b/src/Spout/Reader/Exception/IteratorNotRewindableException.php new file mode 100644 index 0000000..0277fa3 --- /dev/null +++ b/src/Spout/Reader/Exception/IteratorNotRewindableException.php @@ -0,0 +1,12 @@ +zip = new \ZipArchive(); + + if ($this->zip->open($filePath) === true) { + $this->sheetIterator = new SheetIterator($filePath); + } else { + throw new IOException("Could not open $filePath for reading."); + } + } + + /** + * Returns an iterator to iterate over sheets. + * + * @return SheetIterator To iterate over sheets + */ + public function getConcreteSheetIterator() + { + return $this->sheetIterator; + } + + /** + * Closes the reader. To be used after reading the file. + * + * @return void + */ + protected function closeReader() + { + if ($this->zip) { + $this->zip->close(); + } + } +} diff --git a/src/Spout/Reader/ODS/RowIterator.php b/src/Spout/Reader/ODS/RowIterator.php new file mode 100644 index 0000000..1130226 --- /dev/null +++ b/src/Spout/Reader/ODS/RowIterator.php @@ -0,0 +1,314 @@ +" element + */ + public function __construct($xmlReader) + { + $this->xmlReader = $xmlReader; + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->escaper = new \Box\Spout\Common\Escaper\ODS(); + } + + /** + * Rewind the Iterator to the first element. + * NOTE: It can only be done once, as it is not possible to read an XML file backwards. + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once + */ + public function rewind() + { + // Because sheet and row data is located in the file, we can't rewind both the + // sheet iterator and the row iterator, as XML file cannot be read backwards. + // Therefore, rewinding the row iterator has been disabled. + if ($this->hasAlreadyBeenRewound) { + throw new IteratorNotRewindableException(); + } + + $this->hasAlreadyBeenRewound = true; + $this->numReadRows = 0; + $this->rowDataBuffer = null; + $this->hasReachedEndOfFile = false; + + $this->next(); + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return (!$this->hasReachedEndOfFile); + } + + /** + * Move forward to next element. Empty rows will be skipped. + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found + * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML + */ + public function next() + { + $rowData = []; + $cellValue = null; + $numColumnsRepeated = 1; + $numCellsRead = 0; + $hasAlreadyReadOneCell = false; + + try { + while ($this->xmlReader->read()) { + if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) { + // Start of a cell description + $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode(); + + $node = $this->xmlReader->expand(); + $currentCellValue = $this->getCellValue($node); + + // process cell N only after having read cell N+1 (see below why) + if ($hasAlreadyReadOneCell) { + for ($i = 0; $i < $numColumnsRepeated; $i++) { + $rowData[] = $cellValue; + } + } + + $cellValue = $currentCellValue; + $numColumnsRepeated = $currentNumColumnsRepeated; + + $numCellsRead++; + $hasAlreadyReadOneCell = true; + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) { + // End of the row description + $isEmptyRow = ($numCellsRead <= 1 && empty($cellValue)); + if ($isEmptyRow) { + // skip empty rows + $this->next(); + return; + } + + // Only add value if the last read cell is not empty or does not need to repeat cell values. + // This is to avoid creating a lot of empty cells, as Excel adds a last empty "" + // with a number-columns-repeated value equals to the number of (supported columns - used columns). + // In Excel, the number of supported columns is 16384, but we don't want to returns rows with always 16384 cells. + if (!empty($cellValue) || $numColumnsRepeated === 1) { + for ($i = 0; $i < $numColumnsRepeated; $i++) { + $rowData[] = $cellValue; + } + + $this->numReadRows++; + } + break; + + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_TABLE)) { + // The closing "" marks the end of the file + $this->hasReachedEndOfFile = true; + break; + } + } + + } catch (XMLProcessingException $exception) { + throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]"); + } + + $this->rowDataBuffer = $rowData; + } + + /** + * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing + */ + protected function getNumColumnsRepeatedForCurrentNode() + { + $numColumnsRepeated = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); + return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1; + } + + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * @TODO Add other types !! + * + * @param \DOMNode $node + * @return string|int|float|bool The value associated with the cell (or empty string if cell's type is undefined) + */ + protected function getCellValue($node) + { + $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); + $pNodeValue = $this->getTextPNodeValue($node); + + switch ($cellType) { + case self::CELL_TYPE_STRING: + return $this->formatStringCellValue($node); + case self::CELL_TYPE_FLOAT: + return $this->formatFloatCellValue($pNodeValue); + case self::CELL_TYPE_BOOLEAN: + return $this->formatBooleanCellValue($pNodeValue); + default: + return ''; + } + } + + /** + * Returns the value of the first "" node within the given node. + * + * @param \DOMNode $node + * @return string Value for the first "" node or empty string if no "" found + */ + protected function getTextPNodeValue($node) + { + $nodeValue = ''; + $pNodes = $node->getElementsByTagName(self::XML_NODE_P); + + if ($pNodes->length > 0) { + $nodeValue = $pNodes->item(0)->nodeValue; + } + + return $nodeValue; + } + + /** + * Returns the cell String value. + * + * @param \DOMNode $node + * @return string The value associated with the cell + */ + protected function formatStringCellValue($node) + { + $pNodeValues = []; + $pNodes = $node->getElementsByTagName(self::XML_NODE_P); + + foreach ($pNodes as $pNode) { + $currentPValue = ''; + + foreach ($pNode->childNodes as $childNode) { + if ($childNode instanceof \DOMText) { + $currentPValue .= $childNode->nodeValue; + } else if ($childNode->nodeName === self::XML_NODE_S) { + $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C); + $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1; + $currentPValue .= str_repeat(' ', $numSpaces); + } + } + + $pNodeValues[] = $currentPValue; + } + + $escapedCellValue = implode("\n", $pNodeValues); + $cellValue = $this->escaper->unescape($escapedCellValue); + return $cellValue; + } + + /** + * Returns the cell Numeric value from string of nodeValue. + * + * @param string $pNodeValue + * @return int|float The value associated with the cell + */ + protected function formatFloatCellValue($pNodeValue) + { + $cellValue = is_int($pNodeValue) ? intval($pNodeValue) : floatval($pNodeValue); + return $cellValue; + } + + /** + * Returns the cell Boolean value from a specific node's Value. + * + * @param string $pNodeValue + * @return bool The value associated with the cell + */ + protected function formatBooleanCellValue($pNodeValue) + { + // !! is similar to boolval() + $cellValue = !!$pNodeValue; + return $cellValue; + } + + /** + * Return the current element, from the buffer. + * @link http://php.net/manual/en/iterator.current.php + * + * @return array|null + */ + public function current() + { + return $this->rowDataBuffer; + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->numReadRows; + } + + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + $this->xmlReader->close(); + } +} diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php new file mode 100644 index 0000000..c023182 --- /dev/null +++ b/src/Spout/Reader/ODS/Sheet.php @@ -0,0 +1,63 @@ +" element + * @param int $sheetIndex Index of the sheet, based on order of creation (zero-based) + * @param string $sheetName Name of the sheet + */ + public function __construct($xmlReader, $sheetIndex, $sheetName) + { + $this->rowIterator = new RowIterator($xmlReader); + $this->index = $sheetIndex; + $this->name = $sheetName; + } + + /** + * @return RowIterator + */ + public function getRowIterator() + { + return $this->rowIterator; + } + + /** + * @return int Index of the sheet, based on order of creation (zero-based) + */ + public function getIndex() + { + return $this->index; + } + + /** + * @return string Name of the sheet + */ + public function getName() + { + return $this->name; + } +} diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php new file mode 100644 index 0000000..f8b9203 --- /dev/null +++ b/src/Spout/Reader/ODS/SheetIterator.php @@ -0,0 +1,135 @@ +filePath = $filePath; + $this->xmlReader = new XMLReader(); + + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ + $this->escaper = new \Box\Spout\Common\Escaper\ODS(); + } + + /** + * Rewind the Iterator to the first element + * @link http://php.net/manual/en/iterator.rewind.php + * + * @return void + * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data + */ + public function rewind() + { + $this->xmlReader->close(); + + $contentXmlFilePath = $this->filePath . '#content.xml'; + if ($this->xmlReader->open('zip://' . $contentXmlFilePath) === false) { + throw new IOException("Could not open \"{$contentXmlFilePath}\"."); + } + + try { + $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); + } catch (XMLProcessingException $exception) { + throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); + } + + $this->currentSheetIndex = 0; + } + + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * + * @return boolean + */ + public function valid() + { + return $this->hasFoundSheet; + } + + /** + * Move forward to next element + * @link http://php.net/manual/en/iterator.next.php + * + * @return void + */ + public function next() + { + $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); + + if ($this->hasFoundSheet) { + $this->currentSheetIndex++; + } + } + + /** + * Return the current element + * @link http://php.net/manual/en/iterator.current.php + * + * @return Sheet + */ + public function current() + { + $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); + $sheetName = $this->escaper->unescape($escapedSheetName); + + return new Sheet($this->xmlReader, $sheetName, $this->currentSheetIndex); + } + + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * + * @return int + */ + public function key() + { + return $this->currentSheetIndex + 1; + } + + /** + * Cleans up what was created to iterate over the object. + * + * @return void + */ + public function end() + { + $this->xmlReader->close(); + } +} diff --git a/src/Spout/Reader/ReaderFactory.php b/src/Spout/Reader/ReaderFactory.php index 0e39f59..3a32094 100644 --- a/src/Spout/Reader/ReaderFactory.php +++ b/src/Spout/Reader/ReaderFactory.php @@ -33,6 +33,9 @@ class ReaderFactory case Type::XLSX: $reader = new XLSX\Reader(); break; + case Type::ODS: + $reader = new ODS\Reader(); + break; default: throw new UnsupportedTypeException('No readers supporting the given type: ' . $readerType); } diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php index fd33359..d48013f 100644 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ b/src/Spout/Reader/Wrapper/XMLReader.php @@ -138,4 +138,22 @@ class XMLReader extends \XMLReader return $wasNextSuccessful; } + + /** + * @param string $nodeName + * @return bool Whether the XML Reader is currently positioned on the starting node with given name + */ + public function isPositionedOnStartingNode($nodeName) + { + return ($this->nodeType === XMLReader::ELEMENT && $this->name === $nodeName); + } + + /** + * @param string $nodeName + * @return bool Whether the XML Reader is currently positioned on the ending node with given name + */ + public function isPositionedOnEndingNode($nodeName) + { + return ($this->nodeType === XMLReader::END_ELEMENT && $this->name === $nodeName); + } } diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php index 9c4f746..6aafb52 100644 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php @@ -83,7 +83,7 @@ class SharedStringsHelper $escaper = new \Box\Spout\Common\Escaper\XLSX(); $sharedStringsFilePath = $this->getSharedStringsFilePath(); - if ($xmlReader->open($sharedStringsFilePath, null, LIBXML_NONET) === false) { + if ($xmlReader->open($sharedStringsFilePath) === false) { throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".'); } diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php index 577d58d..b1c393e 100644 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ b/src/Spout/Reader/XLSX/Helper/SheetHelper.php @@ -101,7 +101,6 @@ class SheetHelper */ protected function getSheetFromXML($sheetDataXMLFilePath, $sheetIndexZeroBased) { - $sheetId = $sheetIndexZeroBased + 1; $sheetName = $this->getDefaultSheetName($sheetDataXMLFilePath); /* @@ -123,7 +122,6 @@ class SheetHelper if (count($sheetNodes) === 1) { $sheetNode = $sheetNodes[0]; - $sheetId = (int) $sheetNode->getAttribute('sheetId'); $escapedSheetName = $sheetNode->getAttribute('name'); /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ @@ -132,7 +130,7 @@ class SheetHelper } } - return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetId, $sheetIndexZeroBased, $sheetName); + return new Sheet($this->filePath, $sheetDataXMLFilePath, $this->sharedStringsHelper, $sheetIndexZeroBased, $sheetName); } /** diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php index ed9db60..5b266e1 100644 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ b/src/Spout/Reader/XLSX/RowIterator.php @@ -77,6 +77,7 @@ class RowIterator implements IteratorInterface $this->sharedStringsHelper = $sharedStringsHelper; $this->xmlReader = new XMLReader(); + /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->escaper = new \Box\Spout\Common\Escaper\XLSX(); } @@ -143,7 +144,7 @@ class RowIterator implements IteratorInterface try { while ($this->xmlReader->read()) { - if ($this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_DIMENSION) { + if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_DIMENSION)) { // Read dimensions of the sheet $dimensionRef = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) if (preg_match('/[A-Z\d]+:([A-Z\d]+)/', $dimensionRef, $matches)) { @@ -151,7 +152,7 @@ class RowIterator implements IteratorInterface $this->numColumns = CellHelper::getColumnIndexFromCellIndex($lastCellIndex) + 1; } - } else if ($this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) { + } else if ($this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_ROW)) { // Start of the row description $isInsideRowTag = true; @@ -164,7 +165,7 @@ class RowIterator implements IteratorInterface } $rowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : []; - } else if ($isInsideRowTag && $this->xmlReader->nodeType === XMLReader::ELEMENT && $this->xmlReader->name === self::XML_NODE_CELL) { + } else if ($isInsideRowTag && $this->xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL)) { // Start of a cell description $currentCellIndex = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX); $currentColumnIndex = CellHelper::getColumnIndexFromCellIndex($currentCellIndex); @@ -172,16 +173,17 @@ class RowIterator implements IteratorInterface $node = $this->xmlReader->expand(); $rowData[$currentColumnIndex] = $this->getCellValue($node); - } else if ($this->xmlReader->nodeType === XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_ROW) { + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_ROW)) { // End of the row description // If needed, we fill the empty cells $rowData = ($this->numColumns !== 0) ? $rowData : CellHelper::fillMissingArrayIndexes($rowData); $this->numReadRows++; break; - } else if ($this->xmlReader->nodeType === XMLReader::END_ELEMENT && $this->xmlReader->name === self::XML_NODE_WORKSHEET) { + } else if ($this->xmlReader->isPositionedOnEndingNode(self::XML_NODE_WORKSHEET)) { // The closing "" marks the end of the file $this->hasReachedEndOfFile = true; + break; } } @@ -192,6 +194,40 @@ class RowIterator implements IteratorInterface $this->rowDataBuffer = $rowData; } + /** + * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. + * + * @param \DOMNode $node + * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) + */ + protected function getCellValue($node) + { + // Default cell type is "n" + $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; + $vNodeValue = $this->getVNodeValue($node); + + if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { + return $vNodeValue; + } + + switch ($cellType) { + case self::CELL_TYPE_INLINE_STRING: + return $this->formatInlineStringCellValue($node); + case self::CELL_TYPE_SHARED_STRING: + return $this->formatSharedStringCellValue($vNodeValue); + case self::CELL_TYPE_STR: + return $this->formatStrCellValue($vNodeValue); + case self::CELL_TYPE_BOOLEAN: + return $this->formatBooleanCellValue($vNodeValue); + case self::CELL_TYPE_NUMERIC: + return $this->formatNumericCellValue($vNodeValue); + case self::CELL_TYPE_DATE: + return $this->formatDateCellValue($vNodeValue); + default: + return null; + } + } + /** * Returns the cell's string value from a node's nested value node * @@ -203,10 +239,7 @@ class RowIterator implements IteratorInterface // for cell types having a "v" tag containing the value. // if not, the returned value should be empty string. $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0); - if ($vNode !== null) { - return $vNode->nodeValue; - } - return ""; + return ($vNode !== null) ? $vNode->nodeValue : ''; } /** @@ -296,40 +329,6 @@ class RowIterator implements IteratorInterface } } - /** - * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. - * - * @param \DOMNode $node - * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) - */ - protected function getCellValue($node) - { - // Default cell type is "n" - $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; - $vNodeValue = $this->getVNodeValue($node); - - if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { - return $vNodeValue; - } - - switch ($cellType) { - case self::CELL_TYPE_INLINE_STRING: - return $this->formatInlineStringCellValue($node); - case self::CELL_TYPE_SHARED_STRING: - return $this->formatSharedStringCellValue($vNodeValue); - case self::CELL_TYPE_STR: - return $this->formatStrCellValue($vNodeValue); - case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($vNodeValue); - case self::CELL_TYPE_NUMERIC: - return $this->formatNumericCellValue($vNodeValue); - case self::CELL_TYPE_DATE: - return $this->formatDateCellValue($vNodeValue); - default: - return null; - } - } - /** * Return the current element, from the buffer. * @link http://php.net/manual/en/iterator.current.php diff --git a/src/Spout/Reader/XLSX/Sheet.php b/src/Spout/Reader/XLSX/Sheet.php index 9510ecd..ce88212 100644 --- a/src/Spout/Reader/XLSX/Sheet.php +++ b/src/Spout/Reader/XLSX/Sheet.php @@ -15,9 +15,6 @@ class Sheet implements SheetInterface /** @var RowIterator To iterate over sheet's rows */ protected $rowIterator; - /** @var int ID of the sheet */ - protected $id; - /** @var int Index of the sheet, based on order of creation (zero-based) */ protected $index; @@ -28,14 +25,12 @@ class Sheet implements SheetInterface * @param string $filePath Path of the XLSX file being read * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml * @param Helper\SharedStringsHelper Helper to work with shared strings - * @param int $sheetId ID of the sheet * @param int $sheetIndex Index of the sheet, based on order of creation (zero-based) * @param string $sheetName Name of the sheet */ - public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetId, $sheetIndex, $sheetName) + public function __construct($filePath, $sheetDataXMLFilePath, $sharedStringsHelper, $sheetIndex, $sheetName) { $this->rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $sharedStringsHelper); - $this->id = $sheetId; $this->index = $sheetIndex; $this->name = $sheetName; } @@ -48,14 +43,6 @@ class Sheet implements SheetInterface return $this->rowIterator; } - /** - * @return int ID of the sheet - */ - public function getId() - { - return $this->id; - } - /** * @return int Index of the sheet, based on order of creation (zero-based) */ diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php new file mode 100644 index 0000000..81d808e --- /dev/null +++ b/tests/Spout/Reader/ODS/ReaderTest.php @@ -0,0 +1,371 @@ +getAllRowsForFile($filePath); + } + + /** + * @return array + */ + public function dataProviderForTestReadForAllWorksheets() + { + return [ + ['one_sheet_with_strings.ods', 2, 3], + ['two_sheets_with_strings.ods', 4, 3], + ]; + } + + /** + * @dataProvider dataProviderForTestReadForAllWorksheets + * + * @param string $resourceName + * @param int $expectedNumOfRows + * @param int $expectedNumOfCellsPerRow + * @return void + */ + public function testReadForAllWorksheets($resourceName, $expectedNumOfRows, $expectedNumOfCellsPerRow) + { + $allRows = $this->getAllRowsForFile($resourceName); + + $this->assertEquals($expectedNumOfRows, count($allRows), "There should be $expectedNumOfRows rows"); + foreach ($allRows as $row) { + $this->assertEquals($expectedNumOfCellsPerRow, count($row), "There should be $expectedNumOfCellsPerRow cells for every row"); + } + } + + /** + * @return void + */ + public function testReadShouldSupportRowWithOnlyOneCell() + { + $allRows = $this->getAllRowsForFile('sheet_with_only_one_cell.ods'); + $this->assertEquals([['foo']], $allRows); + } + + /** + * @return void + */ + public function testReadShouldSupportNumberColumnsRepeated() + { + $allRows = $this->getAllRowsForFile('sheet_with_number_columns_repeated.ods'); + $expectedRows = [ + [ + 'foo', 'foo', 'foo', + '', '', + true, true, + 10.43, 10.43, 10.43, 10.43, + ], + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @return array + */ + public function dataProviderForTestReadWithFilesGeneratedByExternalSoftwares() + { + return [ + ['file_generated_by_libre_office.ods', true], + ['file_generated_by_excel_2010_windows.ods', false], + ['file_generated_by_excel_office_online.ods', false], + ]; + } + + /** + * @dataProvider dataProviderForTestReadWithFilesGeneratedByExternalSoftwares + * The files contain styles, different value types, gaps between cells, + * repeated values, empty row, different number of cells per row. + * + * @param bool $skipLastEmptyValues + * @param string $fileName + * @return void + */ + public function testReadWithFilesGeneratedByExternalSoftwares($fileName, $skipLastEmptyValues) + { + $allRows = $this->getAllRowsForFile($fileName); + + $expectedRows = [ + ['header1','header2','header3','header4'], + ['val11','val12','val13','val14'], + ['val21','','val23','val23'], + ['', 10.43, 29.11], + ]; + + // In the description of the last cell, Excel specifies that the empty value needs to be repeated + // a lot of times (16384 - number of cells used in the row). To avoid creating 16384 cells all the time, + // this cell is skipped alltogether. + if ($skipLastEmptyValues) { + $expectedRows[3][] = ''; + } + + $this->assertEquals($expectedRows, $allRows); + } + + + /** + * @return void + */ + public function testReadShouldSupportAllCellTypes() + { + $allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.ods'); + + $expectedRows = [ + [ + 'ods--11', 'ods--12', + true, false, + 0, 10.43, + '', + ], + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @return void + */ + public function testReadShouldReturnEmptyStringOnUndefinedCellType() + { + $allRows = $this->getAllRowsForFile('sheet_with_undefined_value_type.ods'); + $this->assertEquals([['ods--11', '', 'ods--13']], $allRows); + } + + /** + * @return void + */ + public function testReadShouldSupportMultilineStrings() + { + $allRows = $this->getAllRowsForFile('sheet_with_multiline_string.ods'); + + $expectedRows = [["string\non multiple\nlines!"]]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @return void + */ + public function testReadShouldSkipEmptyRow() + { + $allRows = $this->getAllRowsForFile('sheet_with_empty_row.ods'); + $this->assertEquals(2, count($allRows), 'There should be only 2 rows, because the empty row is skipped'); + + $expectedRows = [ + ['ods--11', 'ods--12', 'ods--13'], + // row skipped here + ['ods--21', 'ods--22', 'ods--23'], + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @return void + */ + public function testReadShouldPreserveSpacing() + { + $allRows = $this->getAllRowsForFile('sheet_with_various_spaces.ods'); + + $expectedRow = [ + ' 4 spaces before and after ', + ' 1 space before and after ', + '2 spaces after ', + ' 2 spaces before', + "3 spaces in the middle\nand 2 spaces in the middle", + ]; + $this->assertEquals([$expectedRow], $allRows); + } + + + /** + * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) + * + * @return void + */ + public function testReadShouldBeProtectedAgainstBillionLaughAttack() + { + $startTime = microtime(true); + $fileName = 'attack_billion_laughs.ods'; + + try { + // using @ to prevent warnings/errors from being displayed + @$this->getAllRowsForFile($fileName); + $this->fail('An exception should have been thrown'); + } catch (IOException $exception) { + $duration = microtime(true) - $startTime; + $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); + + $expectedMaxMemoryUsage = 30 * 1024 * 1024; // 30MB + $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); + } + } + + /** + * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) + * + * @return void + */ + public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() + { + $startTime = microtime(true); + + $fileName = 'attack_quadratic_blowup.ods'; + $allRows = $this->getAllRowsForFile($fileName); + + $this->assertEquals('', $allRows[0][0], 'Entities should not have been expanded'); + + $duration = microtime(true) - $startTime; + $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); + + $expectedMaxMemoryUsage = 30 * 1024 * 1024; // 30MB + $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); + } + + /** + * @return void + */ + public function testReadShouldBeAbleToProcessEmptySheets() + { + $allRows = $this->getAllRowsForFile('sheet_with_no_cells.ods'); + $this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.'); + } + + /** + * @return void + */ + public function testReadShouldSkipFormulas() + { + $allRows = $this->getAllRowsForFile('sheet_with_formulas.ods'); + + $expectedRows = [ + ['val1', 'val2', 'total1', 'total2'], + [10, 20, 30, 21], + [11, 21, 32, 41], + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @expectedException \Box\Spout\Reader\Exception\IteratorNotRewindableException + * + * @return void + */ + public function testReadShouldThrowIfTryingToRewindRowIterator() + { + $resourcePath = $this->getResourcePath('one_sheet_with_strings.ods'); + $reader = ReaderFactory::create(Type::ODS); + $reader->open($resourcePath); + + foreach ($reader->getSheetIterator() as $sheet) { + // start looping throw the rows + foreach ($sheet->getRowIterator() as $row) { + break; + } + + // this will rewind the row iterator + foreach ($sheet->getRowIterator() as $row) { + break; + } + } + } + + /** + * @return void + */ + public function testReadMultipleTimesShouldRewindReader() + { + $allRows = []; + $resourcePath = $this->getResourcePath('two_sheets_with_strings.ods'); + + $reader = ReaderFactory::create(Type::ODS); + $reader->open($resourcePath); + + foreach ($reader->getSheetIterator() as $sheet) { + // do nothing + } + + // this loop should only add the first row of each sheet + foreach ($reader->getSheetIterator() as $sheet) { + foreach ($sheet->getRowIterator() as $row) { + $allRows[] = $row; + break; + } + } + + // this loop should only add the first row of the first sheet + foreach ($reader->getSheetIterator() as $sheet) { + foreach ($sheet->getRowIterator() as $row) { + $allRows[] = $row; + break; + } + + // stop reading more sheets + break; + } + + $reader->close(); + + $expectedRows = [ + ['ods--sheet1--11', 'ods--sheet1--12', 'ods--sheet1--13'], // 1st row, 1st sheet + ['ods--sheet2--11', 'ods--sheet2--12', 'ods--sheet2--13'], // 1st row, 2nd sheet + ['ods--sheet1--11', 'ods--sheet1--12', 'ods--sheet1--13'], // 1st row, 1st sheet + ]; + $this->assertEquals($expectedRows, $allRows); + } + + /** + * @param string $fileName + * @return array All the read rows the given file + */ + private function getAllRowsForFile($fileName) + { + $allRows = []; + $resourcePath = $this->getResourcePath($fileName); + + $reader = ReaderFactory::create(Type::ODS); + $reader->open($resourcePath); + + foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { + foreach ($sheet->getRowIterator() as $rowIndex => $row) { + $allRows[] = $row; + } + } + + $reader->close(); + + return $allRows; + } +} diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php index 1ec4290..eb42b84 100644 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ b/tests/Spout/Reader/XLSX/ReaderTest.php @@ -173,12 +173,13 @@ class ReaderTest extends \PHPUnit_Framework_TestCase */ public function testReadShouldSkipEmptyRows() { - $allRows = $this->getAllRowsForFile('sheet_with_empty_rows.xlsx'); + $allRows = $this->getAllRowsForFile('sheet_with_empty_row.xlsx'); $this->assertEquals(2, count($allRows), 'There should be only 2 rows, because the empty row is skipped'); $expectedRows = [ ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], + // skipped row here ['s1--A3', 's1--B3', 's1--C3', 's1--D3', 's1--E3'], ]; $this->assertEquals($expectedRows, $allRows); diff --git a/tests/Spout/Reader/XLSX/SheetTest.php b/tests/Spout/Reader/XLSX/SheetTest.php index 8f3f9df..3464819 100644 --- a/tests/Spout/Reader/XLSX/SheetTest.php +++ b/tests/Spout/Reader/XLSX/SheetTest.php @@ -24,11 +24,9 @@ class SheetTest extends \PHPUnit_Framework_TestCase $this->assertEquals('CustomName1', $sheets[0]->getName()); $this->assertEquals(0, $sheets[0]->getIndex()); - $this->assertEquals(1, $sheets[0]->getId()); $this->assertEquals('CustomName2', $sheets[1]->getName()); $this->assertEquals(1, $sheets[1]->getIndex()); - $this->assertEquals(2, $sheets[1]->getId()); } /** diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php index 731e873..8146bb7 100644 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ b/tests/Spout/Writer/ODS/WriterTest.php @@ -5,7 +5,6 @@ namespace Box\Spout\Writer\ODS; use Box\Spout\Common\Type; use Box\Spout\Reader\Wrapper\XMLReader; use Box\Spout\TestUsingResource; -use Box\Spout\Writer\Style\StyleBuilder; use Box\Spout\Writer\WriterFactory; /** diff --git a/tests/resources/ods/attack_billion_laughs.ods b/tests/resources/ods/attack_billion_laughs.ods new file mode 100644 index 0000000000000000000000000000000000000000..0c29831fae33a6afa595f431e32ae48b053a79d6 GIT binary patch literal 2681 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 literal 0 HcmV?d00001 diff --git a/tests/resources/ods/file_corrupted.ods b/tests/resources/ods/file_corrupted.ods new file mode 100644 index 0000000000000000000000000000000000000000..bf012eba337cb4bfaf66638a7a4a09db5e7034ef GIT binary patch literal 1735 zcmWIWW@Zs#00F})d3P`aN^k;cU)K;vT~9wZ{Q#&k4hFvjdG{^v+GF1WrJpk~Fo+^6 z%T3J7OiL{;(W}VK**fW9)?ovlw)eGMkE`N{CpLSx{`$IMz1)^0#v%cs#MSC4 zd*7P*vV3w+*?7HMOX1P^^wq~3bC|y6a4Kz@dT&|JlOTIHm5ukGzm>V08)C`*`vr4Y zYT1k{LDJ4Ej83e)>Zbp+N-IJADy?pxKoo4@6-k%{~ zzqy(nHGE!~`dngSWMGg2h7SiYd~#Du5!p+y6gr)7$-cZSDzitN7^Ppov9Z7c^F97i+#e{!-U6 z;8C+ll-ace&;1uKCvVk$^D{=9B{%f`B2Le4$4R?)NKFqk(le0TqV+PJL)PHh*L9w2 zcN?sgFta#nE4(tHyL?vYrF)VaE?t-F$+_qpkYH_c+O48WG3or*o)7oG$W~0&dq~=whH9c$I64v*eo9_pEQXH?5trT(`)*a!<&j)B{?N zADKREPto^NVxQ+&R`F2ALjQ*TyR*mkf9g(UOPBuSzVqklS7_nXnq%3e0}LNMAO?q1 zCNQKb3sPARHgYm3aIhTc%~8A9-V)@Q{qLPc@yvqg9~J9FV!}i?uTR+h(03~P8q>yq zs3D)1v#@j(Fw|Rs#&H2nEiS3dNi7D4eVA|YYy*M4&%+H~NebWcKFpGCb@}DS<=@y( zBpuB9a?s<@vA# z{%>l^wZN+Z3q`LyVVbt*jr_dQ+eK`li*G78?!2?Bn_;%@GnI$wlM*>oSGQe#@**Z) z)YV>M{(^9^-frq*M_OdQ{OqZ&bk_{Qx<)8Z*}nM z_pJS4Pu8BBInB28kAi})@>)69oLifeJ+jIt882R&cb6})Rendq#72#%+$Rbd2Ua`E z9k^r5URsi7n0s*fe)FS;>YC;Y-{aoDGSC0+-5bH%4=!5V{yTa`b9jct-Ma}rz8e=m zcxC$TeU07wbMa|6r~3Q*AAO^mACzZ$YR6KZPi!+b{BGx&pMKu+OX|PcSBuo{^DJlT zKfaQiaZ`+mpf z3by58X&xW_TCdK@JnPwSr_%paJY1sWURd8L@sz7C$}?Un#N1k2zMt{ay$5W)k1H~_ z?6L~@c1zNB1zV_!iJVmWicPkrJMK;RV{+73t4OP&eLA=7oM{>sVr)Lz3&qxD>2%uL zp5yQA=w%oDd`stlxAN3f`QIJSWK=z`PAj(JV>)*9Z{SXry4nc)HvbQJ-F{Oh$U*ws<$~ME`(|wkx$(W?A> zf{kLFE{jTDtou4Y@jrinHzP0w<1Une$r=eD711zl*b8Zd9(5oSt_``!M%WB70=*nV z7~u{q-C(5}x-RrwjnI`3^j9u4kE82C&$$R)2Z6c}`8dFvl?^1v3WRb%S4?IF@c>S^ BxefpT literal 0 HcmV?d00001 diff --git a/tests/resources/ods/file_generated_by_excel_2010_windows.ods b/tests/resources/ods/file_generated_by_excel_2010_windows.ods new file mode 100755 index 0000000000000000000000000000000000000000..955a465290a962068f3dc4231efc35d30eb9414f GIT binary patch literal 3281 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 literal 0 HcmV?d00001 diff --git a/tests/resources/ods/file_generated_by_excel_office_online.ods b/tests/resources/ods/file_generated_by_excel_office_online.ods new file mode 100644 index 0000000000000000000000000000000000000000..6c3f1f0db0ff83d3163140ccf2740d7cb9a058f4 GIT binary patch literal 3054 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 c2FL0+dHs6f7gRg#xh zRg`-uDJv-}sc9=~>S(Dd=xZoy>*y$`zfsaO(9$)~)Hl<8W2kFvX`rQSsH0(~r($fN zYhkQw^dKgd#%9)5CT}dvO)M=fj4eQBR(6)Q4ptyHJ4=9_wS_a#*wNO?4P@nD53+Z3 zee3Mu;%M*c?BMS1Zs+3V;^F;b7|c2W=AHt(zxQ?b4HlgR%g%z8=fTV`F#ir^79G);1e7Y>Kzp2ANJ`(XjDi< zbZ}I{#}L2B&<~%&d}E`+5}&dUjG~ zL1t1+dU9fRdU8QlQpSVi6lLcZ6=bIu=Vulbueh7ZS8FT+}_>a)jQbN-9GT8bFlwQcmLRz zq0xcS$-&9_@xiXK!7r1e1GD1;3p2xGv*Qa3^ON%{bBinO$Kd`m z@Z!q)((1SIbMX8Hc;OPfx-hZ2G`IG3;oJJ+`r6v+=FaA~-R+Il{q2ptz1_9#jajTd388gBu!IXHO|(RX$`hT8*47J_`C=#oU{XHv z_^gcy3utKYdz|~`dK?HSat3-Am{(-I6H~q?Zsg-4bkLlC%0m%8MzHzTVR+)|Gftax zckhIYbXVrfp20MI41sg0Q=M^?%ycIaSrh2i1{Q}BovT*G*h^LI%FBLe^kyz^*7Cd13gPu%EbT>| zkE!>MsoQ*$ytteXvAlOm(5Ig(lTEqZc~!`*XnNYDO-e=i^&2fXScln1ZyuP9ZEX-{ zt$$Ef_svCV4s&R>J85p`^)-hM<-mCXNx2p|!0`6nT3MMDp?Y0bP%6RANN8PKg=xaT z6xeZc_2cOa^C8OlS~benAt$+ZVSgG80)d)deNywp^24oZ@U?={?Plm`Mv!QL;|HYR zRgCell1y;ORe9~;P0L-q<8gfNZQRDe@N}R`;=7xTq$s|*Vmmv}mlS{*8GUAQpiTtg z-8PHOa{Y8y?vgGGv*zBeY~r3=;M~BOzB)rmYVNFV1u<}ctf`)&KdB$vq6t_cx26NC4AW97wq zn`Oekc_KwENqIUg8&1U*THcZBb|gWBXNlB$t<=;aII$ z5;#|ERr71XJwaPowy82LFD=W`rc*O7#LnB8#FFPw8YAuIu~yvO@KjN92oH+FXcqD0 z`x5UMy^q0(D%0^UWyP9cEJ9p?kaI+DQe5c~&Pdk~nlP?uY#Ff%rDdBlNkOf*E*?VQ`+AC$JZj zSK0hhSTqRVhK3en&(__H*50$7IOvr+RTfbNaHn$n>-Ovxu3qlDr~1{oB|8OMuG8x2 z6A7|?sug9v8!4yO+h5s=jhE|lx8tPaGnHJv-4uW~I-nor0qJw&A)-Mi*?K9Bp{5sC z6b7xFJgKL@cf;ncRLGE3m@F-d0|AIaMa^ONeAm!wmaT01>{Sa8CE+`^_ngKUz!m~H zE2PSH>;>4bh!4p7)t){7W|VJaBbSkr!5KC9#b}moh11E{Fi9M|hoE;E8&}%mavkkxuFBIm-p*(~zOCDA|W&m9}KaD@O2B zIJA4G?2>dehbeXBePpSbGmoA<5}AY{KtlG+UP;(M@@0p>MIIL|EbEm#J<6s}Yhey2YVmO# zrSjK-c{Ae~EfToOFIa)B*LuOx_xc=QwE|?|pna-8T&DKvefib7m3`|JA%@>#~YVB;#>X$q_M& zgP&eyM5_X!NF3#Jff)()ep5{w_zVPH%sFwV@5@M}v?%BY_(qq|@Cf%H4uSP?*alP0 zdyi+jm(zE5Vr$i>B%XJBwEKq`ov;q$i%T~`Vq_jfGC6nMvy=Kr2#g&5zUL0IHF9w@ zydrh`bgzXwLemYJ@AnE2;rLOp@qtMjEj{o4)v}*${Qsz`5}l81rWgA-W+HOVsy5)+SgPB&2gf>J1l*3D~Bk@ zq|WPwoIV(7IcLHw>E?lP@g$*FE1Z07L|=cln_zXO7)lKY+)Q~vzde7xGjF4Qay1_i z;qZ!>T$I1+nu@OwYN6--$>c~wvcBW8hK+dX(A)Yf{yNC(x6gDOD5$e@d&BgGvJ+8I ziZyJXf5;l!Ab+||yWI86xqM(Tb>F1>gbF3(YZM>6W;`q|U2z5^5(C>}h9 zoa^2=`^gCvez#>zQZQRIV{h`85U%DWOP+fh2S>RBzwUA|QT(83qU1xwbphQ1Vi}|H zSw1;kJgvE`#`K8WkObB0gX07q+HLPP4TyLnV=NyFyltVr81|jyd3m7}Xm=!O0L&Ev z(nJHi>j=+2Q7AujgOrP?(daK2dDy@+Bl;siBKn11j?fYul8Cc-D(YhyXWO7GZ zF7-F3^&n;@Lluush`wX}Oiu3=V$5^d^tdJ_(g_p)gBu$N&po;ZFGLTq%K}PXSRio4 zroTY-^Ux04hQxy7WTRlSU^cTA+9jSL=QMRO3paE#89ig>rsNV28;mh@ejOP7ZcotG zCiIKGcdUb(SMPgU%PbMZ$zCj!@BN~SwmG}9%5=)N2~knfH=|=~2*y@IBGzz+-(l`S zduCzTFpTz0AWt%4YnYwO8giK_)RGVJHpI-Q`uwRQvPS~xEYKbr*kEKX(PZM+iryxl zHWik+cpQ=hhO}nXp%mlM^L4{t^LH4-laGjFkcqKSj4ByH+)2F)i{T&gszrLy;vHHJj znONBv*aQBrbv-Z!4*$I_+~0U;Yh&wR`>6U~T1NC6J#%XVQvirb*xcUQz!vmhlz&$# z$lk!-;g|0GU38}UO{E4#MgS|o!?@VkF&R17**$V34}lx%ukn3&9^%R4=|TQHO22dG zag=NgfB>t1H{*BiJSGck1E9GH0A$Z-XKXU~aS-UkjOu&H>jxN`5oCgfwxjN=w@CfW z?ymvObdrF+)DrelO);xrCv{@2M2vMgtSWKe0KYf5BCkz_&4`AgPr|Hvn~uAYu|9jA zdv2zNjwpfP*up0}mA1hNNXw-k>&L)>Xf<4!LG7Gy$}8VQ+BV7gRsylRD(o2?VW_bA z`MNtU*U&1QiMpcwZd!!PcVfQaMeh8x3V4GWrRIPd;z%#SP&k=BUwa??207_2*4R@; zuD103)t)!a<31U@Z;t4~hG&)n5py>5zBZQ~==dzV$C$Yu7`Zx(cO5U2MZZa6IgsCJ zhW^Fmx|bfG<4?>-RvFzxEcvPg0Q z6zR<)D+Y*ZS)n|s(vr9HqDs7l0g>bhyVPVfFRHjDL}sN=!sa+XVW^YJQ(wa9eVY1&vl%?fFipGU;f0!LFSPn2_O^@akd_5|Mv zH0PlXm3387!1Z`ofGy^i$yMvDcBZoHx6LzO+gcVw;%436rQmkb z?dMVkH1R2TZnnTk&PU*Od5hQ$l2W7ct|Dkb-k@)27QRAvk- z{Hh)$eRqYZ&H(c<%sg0JJ64D|P4`=r<)YB~f`VC|TSi?!|1D&-_sJ)B!ChaR*V}nW z(l#q+edE)lyXoWbVEIEEV=8MOx2I9kNtc+I{Ap&rtH$w zps%sdMH8Syb6NxK^00CcO+j2$UY~Ix$LWp zddF`93Ze0Eh~(oK-A^Jk4qveD0%AfOvuJ!9Nh3|VH{X2BrAmYwHYD%$hx_D$L0UzZ z3BQq2E#@(=aT5_SnLs3|twhk=;Bz6UOIA|cFx-J#fHqJO@&X29?6pr|A%-37ted@V zO%2UiUDe%r2*J$mnn{0gJTQ>@1xpY9RJ9^%%bT%~4}2_THWXK`u&`|^b)Dxo=&3y5 zA(c%5YaR81AXiaAT%VOJF3>OGCTs^0ofEO%+!`#XUb@YY- z$#LOo5CAaOY?3^|yC5)A&VcKNCV*~M+S$$S3R z9M?ayj%&R-{|c;g8za_r5{AYzvuw9tia2+JyC=g_$BM#kvvr@YagXpAIG-H$5ko*j zK&(D29R3_QAE|qgy^9s#FNm_JX&pZC3KhJhU*4yhCoxFb2+Pp8aNuF4T8&QJ*@kjN zAve|jfoy*E`8$su$Gyl~6jRM&AH9b10YC!KUUSyLx84amE zcMNyud-Y4wx@E9^Pelm1b8)B27Q( z!YW|qv@2a@MyRan^w*l2PH`?(O#?pNGfodvB%oz3K0yvG9lhTh0NLT2S|?S-Dm*DT4zt6|(wdl7&iw$S1?<_(1GaQR8yx zShE68_<6_tqG0g>DrFz?9ntDl4Tz^{p*)7N<-@xYT33X&msb z*>sc1^xvHGY4iOyd1YbKzYbYH1t5rG9Ki7}I5+RH+!spt%-!u~w3CKmcE>=BxB+W; zou-AlcUaf~roEqg;6WK8ffm)(rs>HqqdmO7Y$BU$9^ulpO{gXEjHTgpw+s7b`H~w= zHe~OswxHJ9QDYu1i%MY^XhUC{RLbwN=)!2bflLsAgNPKIGH->uE{5!RYk66R`@YB? z1qt3v1f=q%p+{lPMfqX-ro3vyPb%1q4X)O$jThpbwUs9}OCMj74YTU2&AD zu*&N9$*M%M4@u?hq5!>2{H(eO1i0vi^{CL!gNccr?dBp z*izEVOhd8PfD>;aILE2b2jv;lm|-@TsYCX%+F8Dq)m5j=2Hgv3U~+zRn}*v*z{^`D zH}+Rpj(82p=WF3or3SfzF?3-p6ecpuWLj(bDg0kH`a`98R8*@-c+SR7g!4j71Oz=3 zln_d4tLciryb3(VTcu4iU&ZE}>MLGgKRfncUoLJ&A~-m;nXNp?OGI8-SS)Ocvx(avtJjxM`H2TkM#iKB%oa-8t(83nxrXewHBdl5`U%{HULFpB9@N74 z2`Mv}DPzLl2tGS0e)t8+0(#$_>Vgx?PKf7Vrqscc=#L|*2z2ZV3igK8XG3QVwAgJB zd56)icmYtp(9%NTCkp&2MioBe$)*@K3`U6U({P^}935p*V~+}qebk6kd;(VypO&}w zA~jWB_hzXu-Lwv`#l*{HzKz4xZ}CZFmO#1uc7t)A!A#=XPekJ?b<*oxz)!@uHT=dr z2k&~1a;lXrO*sEbkYG5KsR&()zrY|{#u}wn((wGoGZJs8oR|Z`!Wd=bK~Ai%fl5NUFyE{VRE;N z;5JE8b3DV8)Qi9)$CFrl5_`vha+q*APG$}+g*<(xH2&4DQ)WHCm+K1n;@4DRcubhK zH@gGeh)X@kTKAl}0b@Pi4;wco=q;em=f65uTR;uYar8Bt)y-wD+~3WE|4K4J4)oy+ zFc1)Y54iTvN#?J=r2+N^k6G>S+Zplplm{}7gb^hK zTJ$dDXk)ddm!RbCU`DQMceF@-6Qz!8ZB$I&}Z<1a17cG3mHU>Oa@^Gh4R__ z_*#%U7Ne+tr^tM;pm;bcUki$+e^*)p$Gf4rwjD8&sG=W zjd&4Xvz19ClI4ghe%-bg&91ZETC&HWfc8i3ej02pa@-e&bYDBstEWDurSC1~&dAr* zK~mo{sOnRZZ-WY5zBwc` z!op6zN4{^-#a$x1;mCZWFa0y4y*U2Rsp#U_}c1qbM@$MbnVv6;%bo_YmCm^=FxH?o&$?esMRVQR|vVV9cW zsINgYsi9!F_LZbfQO2Oxw1F}+UKDs!6VXcb<{DCxQn-PVNu${v%XM7f6FBWHstQJ&E2<*IASoxt^gmekVaSVB`Yk$`QJc2tLQ1-n5|v-RwR?7kFovSAQB4-1Cfo@B zWmz40T`@>{;i4vdbbuK_%Q|dH6;{F+s%BM`KNx<*cW8Aa==-Af9qp^#u0G2uK2(ml z4BEqp5^FlUc1p8~iL?trq(n-Q&y1h;AnUJ!Y=)$!4~9M~6+NGnEv-e~IpI$Re&gp+ z7azP)%cF^!pLuQSFqr$Sc;qFwCEN2)+;6Cc{WqHBC0}|Sn4x?Y9xShzH@)|o<5qH~ z0dyL)2c5e@upgM_qcX?MN6@>-F|*=phVE*1&cAOau;UJREu9?-37d^18J{%vX|QT@ zxr(p}8-UNyYn5GZ`ibgY{FjMylxNQ;#`l2K#&ae`@|~L(0$lh2L2ENsIk& zt)Qd+r$0Ywvj43WiQicHNt^xI$|Gs?Q{Mf?%C9uqpY1#nX+NdrH+KGmR{OKDzdLpM z8)LuHZ2z+JW6C@_@q;USB-(xo9om0(^dD%q|2CWc?wt2;jQ&cx{oT1=8Ku9^=Y|J6 z{}Z?#NVp#!{Akv%km`|f`YBiFf8(G2srYM}d_-J7h2;UA{ls7Y)cZB?J|@wh5=Z#p p>c3O!Kec`hB#%M)r+5(mAw(-kLqBA32ne)?kL^SJStEJ8`+r;L2W$WU literal 0 HcmV?d00001 diff --git a/tests/resources/ods/one_sheet_with_strings.ods b/tests/resources/ods/one_sheet_with_strings.ods new file mode 100644 index 0000000000000000000000000000000000000000..c3df611ba1810b2148720017b8421ce6db4ba3c0 GIT binary patch literal 2561 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 literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_empty_row.ods b/tests/resources/ods/sheet_with_empty_row.ods new file mode 100644 index 0000000000000000000000000000000000000000..4763df0bbd15e0770e109dd9b66ebee88df5270c GIT binary patch literal 2571 zcmZ{m2{hDeAIHbomzG=hWlSMEldX_6moTQu7$MsmF*M8!*_xCr(Uhf7)?AV|ON{JU zvvkRlK@75tozM&>WOt|D`@X7k-{*h+=RD{CJm24Q&iDMjpZ|k|v$F96001t46#SU+ z{0Z5d2_OJ4$p--NGQT=|;(gJ0UljrlOR;Xp4?zys%qp@`F-8Y@G(u_5&UHJh;`D>F ztjZ6gq)pl$)h(JH$F&uSWnhAvImK-0{u+XgTr#b`%JvjIu9FVqi0(p&;xay&@+(pwm38gTEC;2iyDt|U z9F%CO8+}~JkJqEz)r+`hZWP8!;X-E2kE%0(;b$CF@dH76v)T8A- z7D{t}1UzMiFw`ivd4GwpKq%~8LJVu58Q1Tojbb4fJl7(Qw%qt{)5Od2%Bvo62RL8l z{}H>rZagh(F}j4m?6AN{lH)OV^Qw3;1SA-WnrtnC!G5m&7F>ryBpqx%t1BN&i$L%_ zq?_%+VK( z()GNaX(BipW8FtKWVK0jwW{)cGP4mHK3gK{FF#v{)Y*fWN@giayz5rn8-N~tze(ny z22sBpnjGICE-!84K%j6`n1}WDH8epc6{NZsn}3HD`F9im09j@fnBD)s!Off@&R>Em z!!8=B{&yP};G(0XaVeeXKw~b?GE*b)<2IZV9^;Dk`JSD!xCZyP5WahhfjpD9CTLn0 z;))G;b+OrW7Q_4k4m+(_RLbjg^-skowGs>hi)EH~((60cj@;FmtUqg>x4587irJhx zQ(LyVTKEc7dX>fd@DZmxS(jRx7J`jo>{8SpsQ{knbjy1ef(5Q&xsMmeQ?sI`Q2IvF zMWeGc@e$l5ZQiXu7=MROou-Q&xg4;to{;l$V`Y zTdB2|JDMaJRC0^+XMg53FMI&9005#)f4G?b;LyHK-*6Hg3&r1s96tY5JA4|)Uj9HK z{I?hf)vgslf*#Dg-l$xDj>daJ8f@ua`&56g^GtN)Bnj6dq;tj20u}3R+9j9g;Uhoy zZcbSnIdNCRR;$kuR@dF*T&DPW!(NdMXF2+u8x{$TtF91DG*?rBfJznS+_)qleX9j9 zQZYUgUy(&&l=A-+zttnw_9 z=ww0+3=_DSRxO!YV-o-6t6aUgK=J+Tgy@VA9yUv+8gd z6(%9-vs_s1sZTx2tzQ>~a*>53cvwF6{%WFzyl~3iPjh2u!FV^`rO%v#%n<0E0w&aa zmQ3>-^5(G2{0N3t4p&72o+{5skGsV=aijC@QKmlGUlBO3FI?1>?1L5$-Mm52UXT;t$-Bo*(3Nfe8mk(aiQ z<&zzA4~Cd~BAYb3iTd@a9RsnV+cpIOV$VZUkxQ<-j`G_^wNm$_x=wuB=^JvV71V?j zxOE7n4*(>bo#WdY P2lKIJ!a405aR7e-7MVx^ literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_formulas.ods b/tests/resources/ods/sheet_with_formulas.ods new file mode 100644 index 0000000000000000000000000000000000000000..a5bce1f1ae67dbd9b406c545841adb054089a046 GIT binary patch literal 8450 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Ⓢ_ zMH3Lr=sGtHk5PaoG2^!FT;PXIGDt2)B9X}2^0UYEwx)FUP-d0Ogi=L57CGBuxK$;; zBpShYF}pn#_jboX!Q<=Ac1LZSJ920tLgr8)9N;y7GV8!nTH{zf#=k?s+9YNN)PdDbTQ z_WT6ax&QRuNw(h}Ye@WF5kYFk^O$k$Y4bZV^wcm3G)iI_3uTaa z{HlKx$sA-@xmah^SLx+RTYMJU=)I&`-_7L46HRRDVatY{HBo&5M#fpPMu%X*4AiOq zt2Ob|iEgBl-dDwicuM_bV7H)v2eAh-!lkrnJ3#H4pqdNh=JzbZ&Ux;)kOks7dMXRq zcZt+PLR%w~vJ4(nIlH^g3cgCZppGmIx|mqYd&p#=6_>$-@B4|5*Ys3x)LuHT&Z^Y+ z%%ME$C9G8bxJWO2PM5sluz=0$#&rkJ2BmWsAd(pKe@$ZUAr^EwaaCbPDfwr>|0aWo z-hQvzZP~(%Ter#(R@kPTr1I3x9_=gUAdccnIc1c(NG(R^oCfx?Qi#m-QAOmyXJ$-o zn}}I8R7q2$ibZjOP>g>6-o^fq;~AX`dJcG7msJ@*F6XNZ`kkmk8wUGkYV(rev?HP0 zNz`H=nG!Z{RG)@`d!@&=dp|0_BOH+{s>I&d7f7*P6X4a5=s8z^OZReWT-3~=Cl~E~ z|5F|-Ho^oRLz=!9EA+EW?1RBrR zox_JXXoSNnHU#iB%5SsW2MDazcq zi4XX5^$}s}pX7Tj@>i{2>x(~BR1wWTRU5yfe9!ZLW`?c_;UBV&s9XL_5C5upedl-b z=bAbCA!mr_?^ojH4?NO$(C>xVA0Pn0HC^#Tln`n3ub@A(NdGkH552#OuRqR%e}nUb zRrfADF6JX!{4C%%sTxU=bCr=A&iKg`_(r8WTF0ybj?Hk5VPMP{a~g3 zjB?FO{g9O3p!_eE>d#>RG}7d6zJ$}o`0rf5Q;y^-P?$|{!c8{_o4S)^XJ3{ z(Jw!V681j`oL?1xULmjZo*yEP_j4BXtKQF!_b)2-zl!X{h*$m3YWl0z&yM8UDgO|H a`+soJiZaNExeNf@LHr;H|KtAG==mSpY+J7Y literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_multiline_string.ods b/tests/resources/ods/sheet_with_multiline_string.ods new file mode 100644 index 0000000000000000000000000000000000000000..6913c243dc22dfe576aac98758b28d4d2b6bdd7f GIT binary patch literal 2549 zcmZ{mc{r47AIHZwh=lC>YadG{j4f-{Q5b_!p~#Hgkfkh#V{I^&D7#3GCNYk+D3g(; zkYpLVu?uC*TI8L2&--#*=e?in`RBRs@9%eA-|PGPJU8qT4J`)%0AK*DM++PGUuD6+ zrv?DH836z$%2#I}bN~t+a4yK(s~FLV9tE>De3G>$yBo@%QjS`Fq}$`H;H?w!1fjzA zS{EL^A7;xAvPyo)Q*uV7X6#|M0}Du@AjOm`)b~0ML6kI|%JHb)J%(KVZbCY-b+YIltD_jJ#9oTG(mpGbn zT0!q(%4Da9WjZKRseOtE@rVD*wwCy(5J3;dI0w%s#6jPhNrT3gSs0?5b*ZpenpmlC zfV{rTz#8N&w_pG(@V^BN4)$D;*A9W&MJriy6{E#x!5z6pd6H9mEF52%B zb84;Z$pXk>KAc87oUqal92*a-ApWM-C6TzcbFoFfYl)s1+Ige$`@#0g$ia3S2hHf1 zf|E(UO#xvmd_5MM3}47}KV-o)Pp_fY{I)hXtyxNEf789D$z6jIaHFyRwxu6gFsF9k z9nVt`$|<`s&M3M5arp&Ek1^PEoxQNrZYcgiV3TwnFOFs{FK{Mjmguo~InzH+m2J?zVuR-%sJ@3pHnar&WTbM;Ha062;FGYyl z_!3o?A*OLazDU^$uD5%&C?QY%{aoFsA9yEBo~^8VJaTB|V;aepAKvg)H|g}n^U$w( zlN!4z+U%NS4Jv_JuUr+gVGpvSqbmRV9R6l{DeCK4*4aSW;vy&gg!M_t8@qOMADM#7 zVa=~@1v=+gJ&`sdv`(pn)3dU=iwnLkiWtidvpBB_4BtVObT_Dl$Ux3(gs3qpm7bxW zf>^AQT9n3y*gja%b6tr5-U;P1GRKC7(XGDmQhF%8m=ksVN{F!1Fx6=(>@8sdGzX}c zf1_h0+fDp)-}2znQ1*S?pgK>zoBZC~O(TM96)f4ei>o@l z$Ea^+t}$y%2liL!0e~$^=qWk>-=gFmU}k8oCl56@QuyyN27tMvuyM@_#$lPV{fROk zoX3xOJE7fOQ2xKBq&BU|qZ!P6cQx$POz8Bo%H^~weWnXuxD_gcvT~rEN-8|(MS;>n z)p?Z+{je&D^@9Rp*EV~!#tc#0vUK(H1^m5lvzmn3Z(9|A3f0`C@?&LpDwRSKmQ~DX zH;j>$gIF1n>2BB3w~=1d+g^;KRq5m>iL-DWBk{_KPs;-1-qz|&-xp{QsM}fnaDM%# zitkEDMhbIrAM_=x&L!XTU(+(V+Md@=66V?(T+8_yF}WKfJyMp4%BJ>n*#EtqyCE)n z)U!QIG1I7Q2|ar!I6}zhmHob8t$ma&Uw>4kC%yMyXT2Vldv;*?|0l9H|(t7seYy(rAs~iL5pt}<<()+(aP6U1{|S{J$=r#vS0S> zWof-_CLS|Fv3hB*>i9A(70-c%YGf8&83e%tTjfyvD=?l%MXgoqY>6ZGPL544nYW^M zP$M<3>RLAlc?DCA!^ySEb8CN>3w(i+MI%I7|oyJ5sQVN{m zX>;Fi$3zi!S=BxevUl*=_(wW&z>)+j}|oF6#q2ec^~7SXF8dx-_)%Fl~W zxu!WWqDt?QW*6*la2o3HRQ5fQ)!cN4Brwe?MSRKZy?Q)3C*Vtjna>P6GzBFuNYtY}*|p7C+V zM6=4G$tB3j>TlYsA8xn|r(0N9w0@EBfukWEb;ZmZz^sbz80G@kZs;cJN7&M1$q8m0 zb;{ckCaN^-r3&t>mqQ(b3E!Nn`!_*lefJoheWine&l9Jeh`02z_o!Hpy>+;LkJu^++TX{6d9PXd!>g4}CfYQVNf zc(tAaZN!y@_*|b>X+i49_x6=rm1O*6f-sp(f_WLzYFxm}vV~mb9&+(IS|85D$0gCT zeI1cI#EPY(gulnl3rIjo8U7k9)NQQ?mi4p*#NddKDLTlx>#1JjE|L&07yT`YPpojy zG*f7-kHt??c5=_;$IgSq?NgbhCOsm+mOijnNDoGbnAbIw%C~>D zJcR#oR32>2<%c6^--sY~SL~zY!ok3($8vc?WCb!Y=DA%Cb>wbXT*<%}`Fzk+g4S^6 zt@)VhL)d4`umD__f4k3j)cJKzjrGt@(E5EJ#SN!tx>kzz7yCGQd&0L0oDR>xE>Teu z{Qqwvlvp2I$L=LZ!hfduk!p+5;27JF`u!8#k7NQg$7IJ@f7JOuKZuh6JSw3{dxXQ2 ss*^Z4QX%Oms=wfHQgw3Vk5tbosv`ivF3|yh?g1%}Ed|bTKZyhQH;tz;O8@`> literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_no_cells.ods b/tests/resources/ods/sheet_with_no_cells.ods new file mode 100644 index 0000000000000000000000000000000000000000..0d30af9b44d9d2b612857e0dc22b6539f34229f4 GIT binary patch literal 6799 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 literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_only_one_cell.ods b/tests/resources/ods/sheet_with_only_one_cell.ods new file mode 100644 index 0000000000000000000000000000000000000000..c678ff67ef57dfe25ec4678e6b9309876850c33b GIT binary patch literal 2531 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 literal 0 HcmV?d00001 diff --git a/tests/resources/ods/sheet_with_various_spaces.ods b/tests/resources/ods/sheet_with_various_spaces.ods new file mode 100644 index 0000000000000000000000000000000000000000..30e3ed234598b501994d4f43f061ab56c2076716 GIT binary patch literal 2833 zcmZ`*2UL?+8xBKOqD4?qkRhw=B_K#4BFYYg6^cLwNuZ1bnt=zjM#I_k8zx?|Yte-{*PZ795<%Kp+qgC?9sl z#0#23zXt3uz~l#}${jy{4_}o)6fRK3ABmHqnf=k$rPSF)YlA6tN0+g%%l8Fwfe;7_ zqQoLXAh6>QC5okGP_G8gma@uX84>IVajcv;7GC3ioB%)BE z9W$iHU;j~&hv|LUid{2e?#YE74^=cY>?uO~sJXQ1+r-8Nb4k?OQjxp{Y26iTaBpTRPnD^DlP&z0nx1gxThs=6Iy4judT z1p08i*Tr*U+0w5BU+K!U_EhH{v#4b;k2!f>A$tyD%50HOo)`r<8MK$DD^?2xELsc> zKBc}&e%7n(vlQ#9Sukw-)9hhF(c0aYBE}vN&YHF> z;(eEzk)^lMz)YzpKGigSwxp2qMk zIk(=VAH1VOQlpOW9etr$xWMMG9NIj8Ju}lcI8)JX^)|jP%HW1Aewne=;-zw4ZlFG4 zt7>GyYb%D?zOk}`WBk@jV=G*ADTxES$?!}ff;o3jG+Vyb(zv}~uQctbSo6}TYUowe zl_C$K7tbTt=7juuR_*HyUIvG?(^eO?zs950ons^MmT;;h8mf2Kz&=pY?t&P{-Jy9t z(_el!Y;m6ScIyHEg``V4K zWJ&V6o$gxg=|AvSEkLu_Dv5K>704arZDQ4tbnTxQE~T1phED9{CGsRzC2+SxMqa_l zFavb=3i+^X$KlZil9;L`Vpc$rgt`N33s^LtCXT&nR1k;ZTk70{DD57(a+e-8jizG2 zAC7uU%t0f=-etI2pZS6F&UI92SA|Z9g0{L&EtFS~yv{wUZ2ghmN*n2C>r@AGRmWh> zvR}LBS5)^2Eq4B)-E%KRMgCJ%%euOwvRbI-$zbM~v5mk;k`Tk{5{FZoC!(2_yc4dv zV)9_uG^l@zBsNa9QfkR+fV~jqR0QtG*LgHM)mO5-URr$EQ}~TYL0$NLcwE4A z%A@CywB;|X?a&Ci_+KRi0?7a+1XRu6gBMuAF54R_n^_sF{&kE8WaTDqQZXOnItTpz z$PNM>-+v74j`8;VR{ucFRi-rgG^;`GF5vmbZ;j1qTc(s5f%W_f=GiViFXnRAPPTnm zm3{V8nYwnW5xz`j>1#H%V@)9P{5aLXhP*JVM@ib8_@SY4lU4GYSj9CqEWdy|`Fmu; zoHmSe*#ueIM^KO$>+&MM3HM`P^W&8)BQc*6Cv2g{(xoFG=S~l!?RCLhpEwYx>*?=o zMU0+lv=-8h0fp!^lCe`sNWv}Zn>w{+wb8{-u=*Asu7rK~5+yhIoQTR`$GUEZwh7h8 zW(@hZ;UzQXFkuOD0fNP<6A@nsnH(WkV0W0JV&sCfh8peN!jW|G(DGZ{|Kw*z%d(f9 z4FnPf_~8NgL8AiQzflsOY>0_eG)%b<{^c+}L zTEfAnArrdW^+P;ioPur@I)ByK&Ndlq-YrM=#mP^9Y%6_%?O4mb=AUYHX15wVigM0UOb{|3|6rjrB014!L>eS{IABII=1lLTc2itTBMcxSPh{qmJKrT;BgHJ%Q-2+jL1y z(T2|V!06O-GE;9TCW7*yKpAUtquxFjMO7Fd(;AIIs#-~MKQ_D`GJ0A^wO@6<<8|XQ z0mbDdvTRbb(LQs4r)WRjI3<7|6=43!lQ+QBjR4f4rf^=61P>=Q-cPnCCd$^#cV}J) zml5FnC8YS63*s1WUsByDjKuw1D}3jl%wzm=f{FmvqkwRHKvZ1d4L=m_Pwo*gT&5a- z!$(K8nNxy;7R{Vq=V{ZW1#8@`*P47*n@nH6{>pOYw~Z1N#x0f9`9qloywFCXE%JM{ zR9}bt*mYA4xaB#QAgztBvnD$vB*TUd&pG~Xf<3kVv-snnAuN|e-hhN*&HGvcC{uY# zdeke$ofk#EOP~1Ua`pIS=*d#rQ$=A!SeSgc@>^G@S3eqcX`Lb#X;!{R#F>|nJV^6~ z(GTg)O4pq`8gAK<7Y-#NCMca5sro_N5)v>eir8hp!7?dRpWu|Uc@)g>5u0f7>agT+ zxhv=iiJz%tLow=meeR$@Pjz%tLF2iWmvMPZN7jJAyS-y%|78t(Voi1`1?h+`ZF82vhRXF3FJZKlp^H+4#d-w0dHo)U1yF zWZ`Y6;+rD55n1p>&mA}UZQ};1yHeesZP(JrU2p`%L(}SpkVzP(2$OWMOCoKn22L)pf^#DNa$p_rx5U>Yg zh=3*15h6_>)JTs=uex#Hz6ZuJlt(Fub6&)#Gzuu(t

@3)hH6kO^rh72yCQInNlY(hsu*>D`RHf$B1I_H z?Hx1&KbaXEe5?_VO~y~RW{i4y$$UyzUrsoa3st^^Tkdygm+0sI<%CI(utHY^LOi^T zWC?QVKQDULaR4ytTd4FYSyW?tOOX35m$LGP3!8jOXUwNdv#q_`>ErZAV_2GrcKj<)F5{)0nRd54WOoO;jg z4lQ0*{aJ?mV~ns~w*TDfC{A8Ix-{^7k7xY|cWL@~#^qYd)36)EMxCp+{d?ZpK-+uSb%Shca`@x+&bRc%gyI6yjg z%Hk_>ypQ(hTMUo^{$zng-pRbdvg*xk2R+!r2Xq@sjkTT5Otj&I;EKyXc=`X!r!Z*EhB<+c7sN_Mp;AI2N8|%WX+}BEpFvxl&7-e7f z5u$3M^Ga6>H_V+FSuHS@c5Wd>?-9NBbb|x(0t+Uo;^SK>ew~XW1$K+_WcPfE@yo)T zqsOu((ys$Zd22p%abP~9anJ44r;2d%-j_2qEplY%`&MxjtYPSGS*%mb7J-j4AQhNi z?u%U4R2uBaeL5$rPU^_@oGVMd@usD~&Q^Z69vzf_9qS{L*5N_x4notq2Z&DUq*UXe z)fP>0Cx3R~td-8TqKdR26=FS$#otxKsGE8beMX`DuK^ivrh3-ea)xFw@PCIL0+_jo z=vULDoEI3~Q_KLs(fz|vt{yk8A~1iISq;AN=35ANJts;;e@Yq8pNcn$#ZozTdS27!qr!p z&>$XH!Ws9v1r<})C4KiNeK={+@y=_64p61Jj~AbWT~xCnRAOH4Pk^Gn;!vj;;1BK3EPO-3XWbTo#AO zU`9Ld1T`Oj6`e77t2sa{W5FXhLDGx296TAifqTf}d(kwIDkMxON~?RJ@}p!VT_otK zKgU1$%qp97Gcy4ICm4JVG5DYmSl6HCiBHz{2!nw1*3}|tD7I%Y(h zf%(avW^0Oh2m#ei*+FLcWuB)+0-Ldg6gTsQBS{erH&+@B-jH`!ok5o2LOkVJ=x>!I z&pRXZdy5mxqIy18$Y`Rp^*kL;=Ic?mLq!Dc?mP<8&bY1HqkH|mG}iT)rq;0LY~ff+ zYE5CCdkd}Cnf2)a(u{`=XFru9ckANgJdAUUcb~u$O5uCu_a>DJA@ARpHz@QbVIuD4 z8JWt~7Kq^oy1Ojzm_$6F-IB#Rn$`JR<<*9A$SlF8tXmUQ13|K;;zVIxeq??XrBe(!Iqd3EXPzy@zk@%n`DX4Bv_ zGVq=g%}K2n8v3Ai$U!yCO@^1X9`4$xNK~8cikf_~Gta48grepA5ppa((CzR{gfVHA zZg+vo-=E_xC?5|?~bV*8TTg%Zh^{rXB%qK3J>nG@b1Wcsj3s_{qT+j0w!Cu4u_ zjXSY|Gw*%abha2=tniD{<>~G)jqWO^BpiMC%0QN^$MG!IBFFSW8adRD8)zrN#_qH# z3z_9Eeh%GQd)ms=dF&6dzKix1^`o6aBtg{#Vcfs$a zR2YY)eY)S9@85*K7jxbp+pqt7p=xgGuWw;#XC_te zy#3?&Tg&#M9}F9RzPgnWaLv^1xz5a^IzhWkcDubV{`T)%)%K)`pM&0~WN$fjb9+%v z@SS@d%^T&{g`IL&blg4X>b>vT=VBvQ`|X@%H`2gPEOjszTO;%|MRx~jhESIZ@P=UT6q+XL*Ef6)(^@3{BD zLe?cyN)}BKY0*0ow4ly&8q?SBQ7M`M`<1*Kf3=_Xp6NfusNH{_8>``kH41D}Vn-Y< zH%Y8JpgKE4t$C%1en%r4o8g5cKpDv+1+2#1ooQkp-Y%c%$YWyM(U=C5I-}5Le1@a$ zm#WyG|HlLWOwQx+g9QBno=gZsmv;k%@sL*+CL*<2hff+3fs?)YEzsj=BEJpWV{Qf@ F003Ae*Sr7# delta 666 zcmX>ucUgWzJ|oMv%GYk2ix~}|tmRC>OklCe|5#)plJcy=tPmkHE)BlT^NL+nR*Tv^ zWn^HO$~ifQORj$I)cE{c1|moAYk#%gG0$Wx|LT+W3${+~5>yMlbHXiiKdreXU!|MszHjb4fx`N2KW5!c>t7Ni zx-jj+k#%)(HHOyJuRfQ5|6DTdtllJXv;GU;pY_U{zVPnK*OZetW7OYZGoOEUgLqWj zfp*^m-8%YD4y!%r)zyClq1m`J?)SXHRC8_*{?O zzBA27JUjCFub|wN1VNh^&ryCSsi_I86SS)pP^ zp-0b0nayq8vgwp{@w%_O?!5TNJ$w3#`IDc;{wrS?pL5mN@chRY_mcJpzL}OjZzoGq z*1I;nWo;_kPI_#}Rf>3crM{(#|MK~T_l|$jyuv=Cj`6GF74Ed4f|f_}97owN)N}B% zzB-?Hj|43a)fcrqc3g*+P|j8I-(n26No1-#Ob WR5p1(p9OkCV&%7C%jW@_%>V$V88-0% diff --git a/tests/resources/xlsx/attack_quadratic_blowup.xlsx b/tests/resources/xlsx/attack_quadratic_blowup.xlsx index a317c18da0ec868dedd71388cfe3c7811c522f2f..7c67c955e66fa3bd05d84103bb54effe29b2fa18 100644 GIT binary patch delta 505 zcmdlezf^8RJ|oLAMn(6{#f%0})^es`Ca~D#e=M>PNqN?fED#|PE)Ahpp!&$_^3{=y z3=Fkw3=9$slNZeY9=+|r@()xe)556V;V;eVGPCvBBZ@z>4g2YsTg{rx!zrKZ~ zotad<^Y)M9Z!O!4elTqO`RZ0iz%^61=Q=Zw>ICgF+3ohe_}jm4Rojy$ehzw{lD*~B z&Fw`w!FTR;G;fq&7k0{B(Q)^jtM|TVpNoxL?YC>@%c*?YQ#a22^6<*Lo%OHRPTGC( z%2u1+$ExpB7T+{X%+)_>5PEe=R`#j$ewp<=8&^%WdvpJ)@B4@wF(Q11ap~H7|48>Z zXY&Q_@H^z!_SosCb)CZ6>{s$`{MCNid#3*sqjvv!ZmfnE)+n$^ zi5+pc+$6E?fa>fFwdR#3`W=mIY=#$(0A(bP6tEh1cczJbc)NV2Baex3M`IdL>Wo60 z@fnV~U#en%{vQwgGuem74-)+Icrqc354=*7Re7gDIQw~Z!HmrxcugUZI9Ze5f)6#G OCg=0pupQ)P00IDhBHWz- delta 642 zcmZ1~w^4pWJ|oM<$Tx1Aix~}|tmRC>OklCe|5#)plJcw{Ss+3pTpE0v=M}rEtQNI- z%E-Vlm2zdsX{;ONsHD(Cu zb_UM5`1bbsBbVE%&gOFRizm+i+-)HvUHEzRx)WjBc7^!*>^&{0x=HiOrkr_6zb4fF zGFa+TUb*><_SF+J{!N>8Pi49KBb~jo)BC@3e_C@(zDhUMec#-90)_S4e$2X?*1sf3 zbYa?sBkStoY7DKbUwtnB{<&n@S-nZ(X8jkwKkJn@ec|1euPG;Q#;Cu+WMANcrmIp8*Qaj=I+ZR8pQ=8ElvHuHt_A^GEdu&z{UG@wpzm zeP^1Fcy{FTUqQJk$z@@y>eqfgSCcb;#=R{@@!yv1O8S>!l2#gLc1392?d=9vvO>j- zLXV!0GMn4FWz#9^;&oql-Ffkkd-n7f^Cv%x{a3y)KIf{j;rWj*?j`LHd^0V5-cFXJ ztaojC%i2`7o%Gm{s}%9@N_|Te|K;-w?;Zc5d4+vO9phKUE8J;81uc){IgYYlNax^X zeRVuTBf#F#x$&2#6xWsby|z;xIA}2bdHQjc1y4n|4BH+ZKIVI3?GHP4wXjd(R1|&R zeYipVSmFWc#|96k7E1KJ@F`K4$uX~Pk1