diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..3319b667 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,78 @@ +version: 2.1 + +jobs: + build: + docker: + - image: lycantropos/cmake:3.9.5 + parameters: + benchmarks: + default: "OFF" + type: string + build-type: + default: Release + type: string + coverage: + default: "OFF" + type: string + cxx-ver: + default: "11" + type: string + samples: + default: "OFF" + type: string + static: + default: "ON" + type: string + steps: + - checkout + - run: cmake -D XLNT_CXX_LANG=<< parameters.cxx-ver >> -D STATIC=<< parameters.static >> -D BENCHMARKS=<< parameters.benchmarks >> -D SAMPLES=<< parameters.samples >> -D COVERAGE=<< parameters.coverage >> -D CMAKE_BUILD_TYPE=<< parameters.build-type >> . + - run: cmake --build . -- -j2 + - run: ./tests/xlnt.test + - when: + condition: + equal: [ "ON", << parameters.samples >> ] + steps: + - run: ./samples/sample-decrypt + - run: ./samples/sample-img2xlsx ./samples/data/penguin.jpg img.xlsx + - run: ./samples/sample-documentation + - when: + condition: + equal: [ "ON", << parameters.benchmarks >> ] + steps: + - run: ./benchmarks/benchmark-styles + - run: ./benchmarks/benchmark-writer + - when: + condition: + equal: [ "ON", << parameters.coverage >> ] + steps: + - run: lcov --directory source/CMakeFiles/xlnt.dir --capture --output-file coverage.info --base-directory ../source --no-external --gcov-tool /usr/bin/gcov-6 + - run: lcov --output-file coverage.info --remove coverage.info source/detail/serialization/miniz.cpp + - run: i=$(dirname $(pwd)) + - run: sed -i "s|$i/||" coverage.info + - run: cd .. + - run: coveralls-lcov build/coverage.info + +workflows: + build: + jobs: + - build: + name: tests + matrix: + parameters: + cxx-ver: + - "11" + - "17" + build-type: + - Release + - Debug + static: + - "ON" + - "OFF" + - build: + name: samples-benchmarks-coverage + cxx-ver: "11" + build-type: Debug + static: "ON" + samples: "ON" + benchmarks: "ON" + coverage: "OFF" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 46619412..00000000 --- a/.travis.yml +++ /dev/null @@ -1,231 +0,0 @@ -# cpp takes longer -language: minimal -sudo: false -dist: trusty - -git: - depth: false - -notifications: - email: false - -# set up build matrix -matrix: - include: - # ============= GCC ================== - # gcc-5, c++11, release build, dynamic linking - - os: linux - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-5 - env: - - CXX_COMPILER=g++-5 - - C_COMPILER=gcc-5 - - CXX_VER=11 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=OFF - - SAMPLES=OFF - - BENCHMARKS=OFF - - RELEASE=OFF - - # ============= GCC ================== - # gcc-6, c++11, release build, dynamic linking - - os: linux - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-6 - env: - - CXX_COMPILER=g++-6 - - C_COMPILER=gcc-6 - - CXX_VER=11 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=OFF - - SAMPLES=OFF - - BENCHMARKS=OFF - - RELEASE=OFF - - # gcc-7, c++14, release build, static linking - - os: linux - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-7 - env: - - CXX_COMPILER=g++-7 - - C_COMPILER=gcc-7 - - CXX_VER=14 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=ON - - SAMPLES=OFF - - BENCHMARKS=OFF - - RELEASE=OFF - - # gcc-8, c++17, release build, static linking, samples + benchmarks compiled and run - - os: linux - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-8 - env: - - CXX_COMPILER=g++-8 - - C_COMPILER=gcc-8 - - CXX_VER=17 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=ON - - SAMPLES=ON - - BENCHMARKS=ON - - RELEASE=ON - - # =========== CLANG ============= - # clang 4, c++11, release build, dynamic linking - - os: linux - compiler: clang - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-4.0 - packages: - - clang-4.0 - env: - - CXX_COMPILER=clang++-4.0 - - C_COMPILER=clang-4.0 - - CXX_VER=11 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=OFF - - SAMPLES=OFF - - BENCHMARKS=OFF - - RELEASE=OFF - - # clang 5, c++14, release build, dynamic linking - - os: linux - compiler: clang - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-5.0 - packages: - - clang-5.0 - env: - - CXX_COMPILER=clang++-5.0 - - C_COMPILER=clang-5.0 - - CXX_VER=14 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=ON - - SAMPLES=OFF - - BENCHMARKS=OFF - - RELEASE=OFF - - # clang 6, c++17, release build, static linking, samples + benchmarks compiled and run - - os: linux - compiler: clang - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-6.0 - packages: - - clang-6.0 - env: - - CXX_COMPILER=clang++-6.0 - - C_COMPILER=clang-6.0 - - CXX_VER=17 - - BUILD_TYPE=Release - - COVERAGE=OFF - - STATIC=ON - - SAMPLES=ON - - BENCHMARKS=ON - - RELEASE=ON - - # ============= CODE COVERAGE =============== - # gcc-6, c++11, debug build, static linking, code coverage enabled - - os: linux - compiler: gcc - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-6 - - lcov - env: - - CXX_COMPILER=g++-6 - - C_COMPILER=gcc-6 - - CXX_VER=11 - - BUILD_TYPE=Debug - - COVERAGE=ON - - STATIC=ON - - SAMPLES=OFF - -before_install: - - export CC=${C_COMPILER} - - export CXX=${CXX_COMPILER} - -install: - - | - if [[ "${COVERAGE}" == "ON" ]]; then - gem install coveralls-lcov; - fi - - ${CXX} --version - - cmake --version - -script: - - mkdir build - - cd build - - cmake .. -DXLNT_CXX_LANG=${CXX_VER} -DSTATIC=$STATIC -DBENCHMARKS=$BENCHMARKS -DSAMPLES=$SAMPLES -DCOVERAGE=$COVERAGE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DRELEASE=$RELEASE - - cmake --build . -- -j2 - - ./tests/xlnt.test - - echo "samples" && echo 'travis_fold:start:samples' - - if [[ "${SAMPLES}" == "ON" ]]; then ./samples/sample-decrypt; fi - - if [[ "${SAMPLES}" == "ON" ]]; then ./samples/sample-img2xlsx "../samples/data/cafe.jpg" "img.xlsx"; fi - - if [[ "${SAMPLES}" == "ON" ]]; then ./samples/sample-documentation; fi - - echo 'travis_fold:end:samples' - - echo "benchmarks" && echo 'travis_fold:start:benchmarks' - - if [[ "${BENCHMARKS}" == "ON" ]]; then ./benchmarks/benchmark-styles; fi - - if [[ "${BENCHMARKS}" == "ON" ]]; then ./benchmarks/benchmark-writer; fi - - echo 'travis_fold:end:benchmarks' - - -after_success: - - | - if [[ "${COVERAGE}" == "ON" ]]; then - lcov --directory source/CMakeFiles/xlnt.dir --capture --output-file coverage.info --base-directory ../source --no-external --gcov-tool /usr/bin/gcov-6 - lcov --output-file coverage.info --remove coverage.info source/detail/serialization/miniz.cpp - i=$(dirname $(pwd)) - sed -i "s|$i/||" coverage.info - cd .. - coveralls-lcov build/coverage.info - fi - -deploy: - provider: releases - api_key: - secure: "EJKEQkbmNFyXAJYOro7/wSlxSns4O4RAmht5PCBYuOgEqIbB1+QbQjNZd18KyCkqaHQEvww4hVGMdcFsTFnsznjGo6NnDtiYTdhiREtdUd6ohUD0eHTZ15ZL7mW1XR8HWqvv33piDX40UI/dFn1XYLnWunaLZvF5WPa9bzTsloaYHbjxoRw5S4fd0VyqZ2w2zuezCCRUezDcNGOxbVTbB3moSh+hpjrNkw7YxoPNm48CwGRVeRj28XuhDjobfRtPSZMb6Y+EDUnrIZI1mKvB1bkL8QR2ndBtqlG0BWvmEcdDuiQnraJk3iRIAU5T/ycyRqR2UKcW2Fv0iVp5SL9VuqhcvIUxTsYXzuNOZ3hKiWRTj6ndG+gDBsD1K5YwScZksAImwvj/VGEla9Q044W3PbYV2GUE++8mtiZUaxbkaP+uDFUBR5eXqAkte9mainwJ3eaMJ7Tx1KCqkzKO011nUasWfmBWxRUA8vm44SfzwzVfjXToLQZGe4KYzF2cHaXo2YIC48vE/DYs3VnVvnrCjNDO8Cr3m+lfdulhjIZlg+WymQcxuJs7LVzCW7mBPNnrUNOOrGCGP4f/hFxBLk8/eJDznIAzbW27Z1zAHn+MlbADRBzC0Y7cmme4Zu2W7Wy9NTvzNsKabOH/Fe1TfGetEEf6mZ5g7Q3+oZPJ2raLFD8=" - file_glob: true - file: build/*.tar.gz - skip_cleanup: true - overwite: true - on: - repo: tfussell/xlnt - tags: true diff --git a/samples/data/penguin.jpg b/samples/data/penguin.jpg new file mode 100644 index 00000000..7bbaac24 Binary files /dev/null and b/samples/data/penguin.jpg differ diff --git a/source/detail/serialization/xlsx_consumer.cpp b/source/detail/serialization/xlsx_consumer.cpp index 62d9c418..071c92c3 100644 --- a/source/detail/serialization/xlsx_consumer.cpp +++ b/source/detail/serialization/xlsx_consumer.cpp @@ -21,6 +21,7 @@ // @license: http://www.opensource.org/licenses/mit-license.php // @author: see AUTHORS file +#include #include #include // for std::accumulate #include @@ -406,171 +407,7 @@ void xlsx_consumer::open(std::istream &source) cell xlsx_consumer::read_cell() { - if (!has_cell()) - { - return cell(nullptr); - } - - auto ws = worksheet(current_worksheet_); - - if (in_element(qn("spreadsheetml", "sheetData"))) - { - expect_start_element(qn("spreadsheetml", "row"), xml::content::complex); // CT_Row - auto row_index = static_cast(std::stoul(parser().attribute("r"))); - auto &row_properties = ws.row_properties(row_index); - - if (parser().attribute_present("ht")) - { - row_properties.height = converter_.deserialise(parser().attribute("ht")); - } - - if (parser().attribute_present("customHeight")) - { - row_properties.custom_height = is_true(parser().attribute("customHeight")); - } - - if (parser().attribute_present("hidden") && is_true(parser().attribute("hidden"))) - { - row_properties.hidden = true; - } - - if (parser().attribute_present(qn("x14ac", "dyDescent"))) - { - row_properties.dy_descent = converter_.deserialise(parser().attribute(qn("x14ac", "dyDescent"))); - } - - if (parser().attribute_present("spans")) - { - row_properties.spans = parser().attribute("spans"); - } - - skip_attributes({"customFormat", "s", "customFont", - "outlineLevel", "collapsed", "thickTop", "thickBot", - "ph"}); - } - - if (!in_element(qn("spreadsheetml", "row"))) - { - return cell(nullptr); - } - - expect_start_element(qn("spreadsheetml", "c"), xml::content::complex); - - auto cell = streaming_ - ? xlnt::cell(streaming_cell_.get()) - : ws.cell(cell_reference(parser().attribute("r"))); - auto reference = cell_reference(parser().attribute("r")); - cell.d_->parent_ = current_worksheet_; - cell.d_->column_ = reference.column_index(); - cell.d_->row_ = reference.row(); - - if (parser().attribute_present("ph")) - { - cell.d_->phonetics_visible_ = parser().attribute("ph"); - } - - auto has_type = parser().attribute_present("t"); - auto type = has_type ? parser().attribute("t") : "n"; - - if (parser().attribute_present("s")) - { - cell.format(target_.format(static_cast(std::stoull(parser().attribute("s"))))); - } - - auto has_value = false; - auto value_string = std::string(); - - auto has_formula = false; - auto has_shared_formula = false; - auto formula_value_string = std::string(); - - while (in_element(qn("spreadsheetml", "c"))) - { - auto current_element = expect_start_element(xml::content::mixed); - - if (current_element == qn("spreadsheetml", "v")) // s:ST_Xstring - { - has_value = true; - value_string = read_text(); - } - else if (current_element == qn("spreadsheetml", "f")) // CT_CellFormula - { - has_formula = true; - - if (parser().attribute_present("t")) - { - has_shared_formula = parser().attribute("t") == "shared"; - } - - skip_attributes({"aca", "ref", "dt2D", "dtr", "del1", - "del2", "r1", "r2", "ca", "si", "bx"}); - - formula_value_string = read_text(); - } - else if (current_element == qn("spreadsheetml", "is")) // CT_Rst - { - expect_start_element(qn("spreadsheetml", "t"), xml::content::simple); - has_value = true; - value_string = read_text(); - expect_end_element(qn("spreadsheetml", "t")); - } - else - { - unexpected_element(current_element); - } - - expect_end_element(current_element); - } - - expect_end_element(qn("spreadsheetml", "c")); - - if (has_formula && !has_shared_formula) - { - cell.formula(formula_value_string); - } - - if (has_value) - { - if (type == "str") - { - cell.d_->value_text_ = value_string; - cell.data_type(cell::type::formula_string); - } - else if (type == "inlineStr") - { - cell.d_->value_text_ = value_string; - cell.data_type(cell::type::inline_string); - } - else if (type == "s") - { - cell.d_->value_numeric_ = converter_.deserialise(value_string); - cell.data_type(cell::type::shared_string); - } - else if (type == "b") // boolean - { - cell.value(is_true(value_string)); - } - else if (type == "n") // numeric - { - cell.value(converter_.deserialise(value_string)); - } - else if (!value_string.empty() && value_string[0] == '#') - { - cell.error(value_string); - } - } - - if (!in_element(qn("spreadsheetml", "row"))) - { - expect_end_element(qn("spreadsheetml", "row")); - - if (!in_element(qn("spreadsheetml", "sheetData"))) - { - expect_end_element(qn("spreadsheetml", "sheetData")); - } - } - - return cell; + return cell(streaming_cell_.get()); } void xlsx_consumer::read_worksheet(const std::string &rel_id) @@ -1411,8 +1248,174 @@ xml::parser &xlsx_consumer::parser() bool xlsx_consumer::has_cell() { - return in_element(qn("spreadsheetml", "row")) - || in_element(qn("spreadsheetml", "sheetData")); + auto ws = worksheet(current_worksheet_); + + while (streaming_cell_ // we're not at the end of the file + && !in_element(qn("spreadsheetml", "row"))) // we're at the end of a row, or between rows + { + if (parser().peek() == xml::parser::event_type::end_element + && stack_.back() == qn("spreadsheetml", "row")) + { + // We're at the end of a row. + expect_end_element(qn("spreadsheetml", "row")); + // ... and keep parsing. + } + + if (parser().peek() == xml::parser::event_type::end_element + && stack_.back() == qn("spreadsheetml", "sheetData")) + { + // End of sheet. Mark it by setting streaming_cell_ to nullptr, so we never get here again. + expect_end_element(qn("spreadsheetml", "sheetData")); + streaming_cell_.reset(nullptr); + break; + } + + expect_start_element(qn("spreadsheetml", "row"), xml::content::complex); // CT_Row + auto row_index = static_cast(std::stoul(parser().attribute("r"))); + auto &row_properties = ws.row_properties(row_index); + + if (parser().attribute_present("ht")) + { + row_properties.height = converter_.deserialise(parser().attribute("ht")); + } + + if (parser().attribute_present("customHeight")) + { + row_properties.custom_height = is_true(parser().attribute("customHeight")); + } + + if (parser().attribute_present("hidden") && is_true(parser().attribute("hidden"))) + { + row_properties.hidden = true; + } + + if (parser().attribute_present(qn("x14ac", "dyDescent"))) + { + row_properties.dy_descent = converter_.deserialise(parser().attribute(qn("x14ac", "dyDescent"))); + } + + if (parser().attribute_present("spans")) + { + row_properties.spans = parser().attribute("spans"); + } + + skip_attributes({"customFormat", "s", "customFont", + "outlineLevel", "collapsed", "thickTop", "thickBot", + "ph"}); + } + + if (!streaming_cell_) + { + // We're at the end of the worksheet + return false; + } + + expect_start_element(qn("spreadsheetml", "c"), xml::content::complex); + + assert(streaming_); + auto cell = xlnt::cell(streaming_cell_.get()); + auto reference = cell_reference(parser().attribute("r")); + cell.d_->parent_ = current_worksheet_; + cell.d_->column_ = reference.column_index(); + cell.d_->row_ = reference.row(); + + if (parser().attribute_present("ph")) + { + cell.d_->phonetics_visible_ = parser().attribute("ph"); + } + + auto has_type = parser().attribute_present("t"); + auto type = has_type ? parser().attribute("t") : "n"; + + if (parser().attribute_present("s")) + { + cell.format(target_.format(static_cast(std::stoull(parser().attribute("s"))))); + } + + auto has_value = false; + auto value_string = std::string(); + + auto has_formula = false; + auto has_shared_formula = false; + auto formula_value_string = std::string(); + + while (in_element(qn("spreadsheetml", "c"))) + { + auto current_element = expect_start_element(xml::content::mixed); + + if (current_element == qn("spreadsheetml", "v")) // s:ST_Xstring + { + has_value = true; + value_string = read_text(); + } + else if (current_element == qn("spreadsheetml", "f")) // CT_CellFormula + { + has_formula = true; + + if (parser().attribute_present("t")) + { + has_shared_formula = parser().attribute("t") == "shared"; + } + + skip_attributes({"aca", "ref", "dt2D", "dtr", "del1", + "del2", "r1", "r2", "ca", "si", "bx"}); + + formula_value_string = read_text(); + } + else if (current_element == qn("spreadsheetml", "is")) // CT_Rst + { + expect_start_element(qn("spreadsheetml", "t"), xml::content::simple); + has_value = true; + value_string = read_text(); + expect_end_element(qn("spreadsheetml", "t")); + } + else + { + unexpected_element(current_element); + } + + expect_end_element(current_element); + } + + expect_end_element(qn("spreadsheetml", "c")); + + if (has_formula && !has_shared_formula) + { + cell.formula(formula_value_string); + } + + if (has_value) + { + if (type == "str") + { + cell.d_->value_text_ = value_string; + cell.data_type(cell::type::formula_string); + } + else if (type == "inlineStr") + { + cell.d_->value_text_ = value_string; + cell.data_type(cell::type::inline_string); + } + else if (type == "s") + { + cell.d_->value_numeric_ = converter_.deserialise(value_string); + cell.data_type(cell::type::shared_string); + } + else if (type == "b") // boolean + { + cell.value(is_true(value_string)); + } + else if (type == "n") // numeric + { + cell.value(converter_.deserialise(value_string)); + } + else if (!value_string.empty() && value_string[0] == '#') + { + cell.error(value_string); + } + } + + return true; } std::vector xlsx_consumer::read_relationships(const path &part) diff --git a/source/detail/serialization/xlsx_consumer.hpp b/source/detail/serialization/xlsx_consumer.hpp index c9a987ee..2dbafa01 100644 --- a/source/detail/serialization/xlsx_consumer.hpp +++ b/source/detail/serialization/xlsx_consumer.hpp @@ -413,8 +413,6 @@ private: std::unique_ptr streaming_cell_; - detail::cell_impl *current_cell_; - detail::worksheet_impl *current_worksheet_; number_serialiser converter_; }; diff --git a/tests/data/Issue492_empty_row.xlsx b/tests/data/Issue492_empty_row.xlsx new file mode 100644 index 00000000..32b6da5a Binary files /dev/null and b/tests/data/Issue492_empty_row.xlsx differ diff --git a/tests/workbook/serialization_test_suite.cpp b/tests/workbook/serialization_test_suite.cpp index 289e08ca..b6c29581 100644 --- a/tests/workbook/serialization_test_suite.cpp +++ b/tests/workbook/serialization_test_suite.cpp @@ -93,6 +93,7 @@ public: register_test(test_load_save_german_locale); register_test(test_Issue445_inline_str_load); register_test(test_Issue445_inline_str_streaming_read); + register_test(test_Issue492_stream_empty_row); } bool workbook_matches_file(xlnt::workbook &wb, const xlnt::path &file) @@ -733,8 +734,25 @@ public: xlnt::streaming_workbook_reader wbr; wbr.open(path_helper::test_file("Issue445_inline_str.xlsx")); wbr.begin_worksheet("Sheet"); + xlnt_assert(wbr.has_cell()); auto cell = wbr.read_cell(); xlnt_assert_equals(cell.value(), std::string("a")); } + + void test_Issue492_stream_empty_row() + { + xlnt::streaming_workbook_reader wbr; + wbr.open(path_helper::test_file("Issue492_empty_row.xlsx")); + wbr.begin_worksheet("BLS Data Series"); + xlnt_assert(wbr.has_cell()); + xlnt_assert_equals(wbr.read_cell().reference(), "A1"); + xlnt_assert(wbr.has_cell()); + xlnt_assert_equals(wbr.read_cell().reference(), "A2"); + xlnt_assert(wbr.has_cell()); + xlnt_assert_equals(wbr.read_cell().reference(), "A4"); + xlnt_assert(wbr.has_cell()); + xlnt_assert_equals(wbr.read_cell().reference(), "B4"); + xlnt_assert(!wbr.has_cell()); + } }; static serialization_test_suite x;