diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 90d233f..af25849 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -41,7 +41,7 @@ jobs: lcov --remove coverage.info '/usr/*' --output-file coverage.info lcov --list coverage.info - name: Upload coverage to codecov - uses: codecov/codecov-action@5c93f7ab87f1aa9b956609bbc4b50a6e747fe2fb + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.info diff --git a/src/las_reader.hpp b/src/las_reader.hpp index 9d3ef9a..c5811fb 100644 --- a/src/las_reader.hpp +++ b/src/las_reader.hpp @@ -116,14 +116,14 @@ class LASReader { if (record.is_ogc_math_transform_wkt()) { std::vector wkt(record.record_length_after_header); LASPP_CHECK_READ(*m_input_stream, wkt.data(), record.record_length_after_header); - std::string wkt_string(wkt.begin(), wkt.end()); + std::string wkt_string(las_packed_string(std::string_view(wkt.data(), wkt.size()))); LASPP_ASSERT(!m_math_wkt.has_value(), "Multiple math WKTs found in header"); m_math_wkt.emplace(wkt_string); } if (record.is_ogc_coordinate_system_wkt()) { std::vector wkt(record.record_length_after_header); LASPP_CHECK_READ(*m_input_stream, wkt.data(), record.record_length_after_header); - std::string wkt_string(wkt.data(), wkt.size() - 1); + std::string wkt_string(las_packed_string(std::string_view(wkt.data(), wkt.size()))); LASPP_ASSERT(!m_coordinate_wkt.has_value(), "Multiple coordinate WKTs found in header"); m_coordinate_wkt.emplace(wkt_string); } diff --git a/src/tests/test_reader.cpp b/src/tests/test_reader.cpp index 4d9a304..10f5201 100644 --- a/src/tests/test_reader.cpp +++ b/src/tests/test_reader.cpp @@ -100,6 +100,83 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { } } + { + // Some LAS writers store WKT without a trailing null byte, using the full + // VLR record length for the WKT text (record_id 2112). + const std::string wkt = "WKT_WITHOUT_NULL_BYTE"; + std::stringstream stream; + { + LASWriter writer(stream, 0, 0); + + LASVLR wkt_vlr; + wkt_vlr.reserved = 0; + string_to_arr("LASF_Projection", wkt_vlr.user_id); + wkt_vlr.record_id = 2112; + wkt_vlr.record_length_after_header = static_cast(wkt.size()); + string_to_arr("OGC WKT", wkt_vlr.description); + writer.write_vlr(wkt_vlr, + std::span(reinterpret_cast(wkt.data()), wkt.size())); + + std::vector points(1); + writer.write_points(std::span(points)); + } + + LASReader reader(stream); + LASPP_ASSERT_EQ(reader.coordinate_wkt().value(), wkt); + LASPP_ASSERT(!reader.math_wkt().has_value()); + } + + { + // Test math transform WKT (record_id 2111) written via write_wkt with the + // math_transform_wkt flag. The writer includes a trailing null byte. + const std::string math_wkt = "MATH_TRANSFORM_WKT"; + std::stringstream stream; + { + LASWriter writer(stream, 0, 0); + + // Write coordinate WKT first so we can verify wkt() returns it preferentially + writer.write_wkt("COORDINATE_WKT"); + writer.write_wkt(math_wkt, true); + + std::vector points(1); + writer.write_points(std::span(points)); + } + + LASReader reader(stream); + LASPP_ASSERT_EQ(reader.coordinate_wkt().value(), "COORDINATE_WKT"); + LASPP_ASSERT_EQ(reader.math_wkt().value(), math_wkt); + // wkt() should return coordinate_wkt when both are present + LASPP_ASSERT_EQ(reader.wkt().value(), "COORDINATE_WKT"); + } + + { + // Math transform WKT without a trailing null byte (record_id 2111), matching + // the no-null-byte pattern tested for record_id 2112 above. + const std::string math_wkt = "MATH_WKT_NO_NULL"; + std::stringstream stream; + { + LASWriter writer(stream, 0, 0); + + LASVLR wkt_vlr; + wkt_vlr.reserved = 0; + string_to_arr("LASF_Projection", wkt_vlr.user_id); + wkt_vlr.record_id = 2111; + wkt_vlr.record_length_after_header = static_cast(math_wkt.size()); + string_to_arr("OGC WKT", wkt_vlr.description); + writer.write_vlr( + wkt_vlr, std::span(reinterpret_cast(math_wkt.data()), math_wkt.size())); + + std::vector points(1); + writer.write_points(std::span(points)); + } + + LASReader reader(stream); + LASPP_ASSERT_EQ(reader.math_wkt().value(), math_wkt); + LASPP_ASSERT(!reader.coordinate_wkt().has_value()); + // wkt() falls back to math_wkt when coordinate_wkt is absent + LASPP_ASSERT_EQ(reader.wkt().value(), math_wkt); + } + { // Format 5 includes WavePacketData which is not supported in compressed mode // So we only test uncompressed format 5 diff --git a/src/vlr.hpp b/src/vlr.hpp index 9d71bf6..d844142 100644 --- a/src/vlr.hpp +++ b/src/vlr.hpp @@ -147,6 +147,8 @@ class LASGeoKeys { }; // Fixed-width LAS string (user_id, description, ...): may omit a trailing NUL. +// Also used for WKT VLR payloads where some writers omit the trailing null and +// store the WKT in the full record_length_after_header bytes. inline std::string_view las_packed_string(std::string_view raw) { const auto z = raw.find('\0'); return z == std::string_view::npos ? raw : raw.substr(0, z);