diff --git a/mdal/frmts/mdal_selafin.cpp b/mdal/frmts/mdal_selafin.cpp index c3a838c8..b57b0687 100644 --- a/mdal/frmts/mdal_selafin.cpp +++ b/mdal/frmts/mdal_selafin.cpp @@ -526,9 +526,8 @@ int MDAL::SelafinFile::readInt( ) { unsigned char data[4]; - if ( mIn.read( reinterpret_cast< char * >( &data ), 4 ) ) - if ( !mIn ) - throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "Unable to open stream for reading int" ); + if ( !mIn.read( reinterpret_cast< char * >( &data ), 4 ) ) + throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "Unable to read int, stream failed" ); if ( mChangeEndianness ) { std::reverse( reinterpret_cast< char * >( &data ), reinterpret_cast< char * >( &data ) + 4 ); @@ -810,18 +809,33 @@ MDAL::MeshSelafinVertexIterator::MeshSelafinVertexIterator( std::shared_ptrverticesCount() - mPosition ); + // The source file is read lazily here, through the C API, which has no + // exception handling: a file truncated or rewritten under an open handle + // must be reported as "nothing read", never let terminate the caller. + try + { + size_t count = std::min( vertexCount, mReader->verticesCount() - mPosition ); - if ( count == 0 ) - return 0; + if ( count == 0 ) + return 0; - std::vector coord = mReader->vertices( mPosition, count ); + std::vector coord = mReader->vertices( mPosition, count ); - memcpy( coordinates, coord.data(), count * 24 ); + memcpy( coordinates, coord.data(), count * 24 ); - mPosition += count; + mPosition += count; - return count; + return count; + } + catch ( MDAL::Error &err ) + { + MDAL::Log::error( err, "SELAFIN" ); + } + catch ( MDAL_Status status ) + { + MDAL::Log::error( status, "SELAFIN", "error occurred while reading vertices" ); + } + return 0; } MDAL::MeshSelafinFaceIterator::MeshSelafinFaceIterator( std::shared_ptr reader ): @@ -832,39 +846,53 @@ size_t MDAL::MeshSelafinFaceIterator::next( size_t faceOffsetsBufferLen, int *fa { assert( faceOffsetsBuffer ); assert( vertexIndicesBuffer ); - assert( mReader->verticesPerFace() != 0 ); - const size_t verticesPerFace = mReader->verticesPerFace(); - size_t count = std::min( faceOffsetsBufferLen, mReader->facesCount() - mPosition ); + // see MeshSelafinVertexIterator::next: reading here must not throw through + // the C API + try + { + assert( mReader->verticesPerFace() != 0 ); - count = std::min( count, vertexIndicesBufferLen / verticesPerFace ); + const size_t verticesPerFace = mReader->verticesPerFace(); + size_t count = std::min( faceOffsetsBufferLen, mReader->facesCount() - mPosition ); - if ( count == 0 ) - return 0; + count = std::min( count, vertexIndicesBufferLen / verticesPerFace ); - std::vector indexes = mReader->connectivityIndex( mPosition * verticesPerFace, count * verticesPerFace ); + if ( count == 0 ) + return 0; - if ( indexes.size() != count * verticesPerFace ) - throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading faces" ); + std::vector indexes = mReader->connectivityIndex( mPosition * verticesPerFace, count * verticesPerFace ); - int vertexLocalIndex = 0; + if ( indexes.size() != count * verticesPerFace ) + throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading faces" ); - for ( size_t i = 0; i < count; i++ ) - { - for ( size_t j = 0; j < verticesPerFace; ++j ) + int vertexLocalIndex = 0; + + for ( size_t i = 0; i < count; i++ ) { - if ( size_t( indexes[j + i * verticesPerFace] ) > mReader->verticesCount() ) - throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading faces" ); - vertexIndicesBuffer[vertexLocalIndex + j] = indexes[j + i * verticesPerFace] - 1; + for ( size_t j = 0; j < verticesPerFace; ++j ) + { + if ( size_t( indexes[j + i * verticesPerFace] ) > mReader->verticesCount() ) + throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading faces" ); + vertexIndicesBuffer[vertexLocalIndex + j] = indexes[j + i * verticesPerFace] - 1; + } + vertexLocalIndex += MDAL::toInt( verticesPerFace ); + faceOffsetsBuffer[i] = vertexLocalIndex; } - vertexLocalIndex += MDAL::toInt( verticesPerFace ); - faceOffsetsBuffer[i] = vertexLocalIndex; - } - mPosition += count; - - return count; + mPosition += count; + return count; + } + catch ( MDAL::Error &err ) + { + MDAL::Log::error( err, "SELAFIN" ); + } + catch ( MDAL_Status status ) + { + MDAL::Log::error( status, "SELAFIN", "error occurred while reading faces" ); + } + return 0; } MDAL::DatasetSelafin::DatasetSelafin( MDAL::DatasetGroup *parent, @@ -877,32 +905,60 @@ MDAL::DatasetSelafin::DatasetSelafin( MDAL::DatasetGroup *parent, size_t MDAL::DatasetSelafin::scalarData( size_t indexStart, size_t count, double *buffer ) { - count = std::min( mReader->verticesCount() - indexStart, count ); - std::vector values = mReader->datasetValues( mTimeStepIndex, mXVariableIndex, indexStart, count ); - if ( values.size() != count ) - throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading dataset value" ); + // see MeshSelafinVertexIterator::next: reading here must not throw through + // the C API + try + { + count = std::min( mReader->verticesCount() - indexStart, count ); + std::vector values = mReader->datasetValues( mTimeStepIndex, mXVariableIndex, indexStart, count ); + if ( values.size() != count ) + throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading dataset value" ); - memcpy( buffer, values.data(), count * 8 ); + memcpy( buffer, values.data(), count * 8 ); - return count; + return count; + } + catch ( MDAL::Error &err ) + { + MDAL::Log::error( err, "SELAFIN" ); + } + catch ( MDAL_Status status ) + { + MDAL::Log::error( status, "SELAFIN", "error occurred while reading dataset values" ); + } + return 0; } size_t MDAL::DatasetSelafin::vectorData( size_t indexStart, size_t count, double *buffer ) { - count = std::min( mReader->verticesCount() - indexStart, count ); - std::vector xValues = mReader->datasetValues( mTimeStepIndex, mXVariableIndex, indexStart, count ); - std::vector yValues = mReader->datasetValues( mTimeStepIndex, mYVariableIndex, indexStart, count ); + // see MeshSelafinVertexIterator::next: reading here must not throw through + // the C API + try + { + count = std::min( mReader->verticesCount() - indexStart, count ); + std::vector xValues = mReader->datasetValues( mTimeStepIndex, mXVariableIndex, indexStart, count ); + std::vector yValues = mReader->datasetValues( mTimeStepIndex, mYVariableIndex, indexStart, count ); - if ( xValues.size() != count || yValues.size() != count ) - throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading dataset value" ); + if ( xValues.size() != count || yValues.size() != count ) + throw MDAL::Error( MDAL_Status::Err_UnknownFormat, "File format problem while reading dataset value" ); - for ( size_t i = 0; i < count; ++i ) + for ( size_t i = 0; i < count; ++i ) + { + buffer[2 * i] = xValues[i]; + buffer[2 * i + 1] = yValues[i]; + } + + return count; + } + catch ( MDAL::Error &err ) { - buffer[2 * i] = xValues[i]; - buffer[2 * i + 1] = yValues[i]; + MDAL::Log::error( err, "SELAFIN" ); } - - return count; + catch ( MDAL_Status status ) + { + MDAL::Log::error( status, "SELAFIN", "error occurred while reading dataset values" ); + } + return 0; } void MDAL::DatasetSelafin::setXVariableIndex( size_t index ) @@ -951,19 +1007,41 @@ static void writeValueArray( std::ofstream &file, const std::vector &array ) writeValue( file, value ); } -template -static void writeVertices( std::ofstream &file, MDAL::Mesh *mesh ) +// Reads the whole connectivity table of the mesh as 1-based indices (SELAFIN +// convention). +static void readConnectivity( MDAL::Mesh *mesh, size_t facesCount, size_t verticesPerFace, + std::vector &connectivity ) { + connectivity.reserve( facesCount * verticesPerFace ); + if ( facesCount == 0 ) + return; + const int bufSize = BUFFER_SIZE; + std::vector faceOffsetBuffer( bufSize ); + std::vector inkle( bufSize * verticesPerFace ); + std::unique_ptr faceIter = mesh->readFaces(); + size_t count = 0; + do + { + count = faceIter->next( bufSize, faceOffsetBuffer.data(), bufSize * verticesPerFace, inkle.data() ); + const size_t n = count * verticesPerFace; + for ( size_t i = 0; i < n; ++i ) + connectivity.push_back( inkle[i] + 1 ); // SELAFIN is 1-based + } + while ( count != 0 ); +} + +static void readVerticesXY( MDAL::Mesh *mesh, std::vector &xValues, std::vector &yValues ) +{ + const size_t verticesCount = mesh->verticesCount(); + xValues.resize( verticesCount ); + yValues.resize( verticesCount ); + const size_t bufferSize = BUFFER_SIZE; + std::vector coordinates( bufferSize * 3 ); std::unique_ptr vertexIter = mesh->readVertices(); - size_t verticesCount = mesh->verticesCount(); - std::vector xValues( verticesCount ); - std::vector yValues( verticesCount ); size_t count = 0; size_t vertexIndex = 0; - size_t bufferSize = BUFFER_SIZE; do { - std::vector coordinates( bufferSize * 3 ); count = vertexIter->next( bufferSize, coordinates.data() ); for ( size_t i = 0; i < count; ++i ) { @@ -973,14 +1051,40 @@ static void writeVertices( std::ofstream &file, MDAL::Mesh *mesh ) vertexIndex += count; } while ( count != 0 ); - writeValueArrayRecord( file, xValues ); - writeValueArrayRecord( file, yValues ); } -void MDAL::DriverSelafin::save( const std::string &fileName, const std::string &, MDAL::Mesh *mesh ) +// Everything that has to be READ from the mesh before the output file is +// opened: a MeshSelafin reads its frame lazily from its own source file, +// which may be the file about to be written. +struct SelafinMeshFrame { - std::ofstream file = MDAL::openOutputFile( fileName.c_str(), std::ofstream::binary ); + size_t verticesCount = 0; + size_t facesCount = 0; + size_t verticesPerFace = 0; + std::vector connectivity; // 1-based + std::vector x; + std::vector y; + std::vector ipobo; +}; + +static SelafinMeshFrame collectMeshFrame( MDAL::Mesh *mesh ) +{ + SelafinMeshFrame frame; + frame.verticesPerFace = mesh->faceVerticesMaximumCount(); + frame.verticesCount = mesh->verticesCount(); + frame.facesCount = mesh->facesCount(); + + readConnectivity( mesh, frame.facesCount, frame.verticesPerFace, frame.connectivity ); + readVerticesXY( mesh, frame.x, frame.y ); + // IPOBO filled with 0 + frame.ipobo = std::vector( frame.verticesCount, 0 ); + + return frame; +} + +static void writeMeshFrame( std::ofstream &file, const SelafinMeshFrame &frame ) +{ std::string header( "Selafin file created by MDAL library" ); std::string remainingStr( 72 - header.size(), ' ' ); header.append( remainingStr ); @@ -1001,45 +1105,133 @@ void MDAL::DriverSelafin::save( const std::string &fileName, const std::string & writeValueArrayRecord( file, param ); //NELEM,NPOIN,NDP,1 - size_t verticesPerFace = mesh->faceVerticesMaximumCount(); - size_t verticesCount = mesh->verticesCount(); - size_t facesCount = mesh->facesCount(); std::vector elem( 4 ); - elem[0] = MDAL::toInt( facesCount ); - elem[1] = MDAL::toInt( verticesCount ); - elem[2] = MDAL::toInt( verticesPerFace ); + elem[0] = MDAL::toInt( frame.facesCount ); + elem[1] = MDAL::toInt( frame.verticesCount ); + elem[2] = MDAL::toInt( frame.verticesPerFace ); elem[3] = 1; writeValueArrayRecord( file, elem ); - //connectivity table - int bufferSize = BUFFER_SIZE; - std::vector faceOffsetBuffer( bufferSize ); - std::unique_ptr faceIter = mesh->readFaces(); - size_t count = 0; - writeInt( file, MDAL::toInt( facesCount * verticesPerFace * 4 ) ); - if ( facesCount > 0 ) + writeValueArrayRecord( file, frame.connectivity ); + writeValueArrayRecord( file, frame.ipobo ); + writeValueArrayRecord( file, frame.x ); + writeValueArrayRecord( file, frame.y ); +} + +// Replaces `target` with the content of `tmp` as safely as the platform allows. +// Returns true on success; the temporary file is then gone. +// +// std::rename() replaces the target atomically on POSIX, so it is tried first; +// MSVC's _wrename fails when the destination exists, hence the fallback to +// removing the target first and, when it cannot be removed because another +// handle holds it open (the QGIS provider keeps its own MDAL handle on the +// layer file while the layer is saved), to rewriting its content in place. +// +// On failure `error` describes the problem and the temporary file is removed, +// unless it holds the only copy of the new content because the target has +// already been removed or truncated; `error` then names the file kept on disk. +static bool replaceFile( const std::string &tmp, const std::string &target, std::string &error ) +{ + if ( MDAL::renameFile( tmp, target ) ) + return true; + + if ( MDAL::fileExists( target ) && MDAL::deleteFile( target ) ) { - do - { - std::vector inkle( bufferSize * verticesPerFace ); - count = faceIter->next( bufferSize, faceOffsetBuffer.data(), bufferSize * verticesPerFace, inkle.data() ); - inkle.resize( count * verticesPerFace ); - for ( size_t i = 0; i < inkle.size(); ++i ) - inkle[i]++; + if ( MDAL::renameFile( tmp, target ) ) + return true; + error = "Unable to write file " + target + ", new content kept as " + tmp; + return false; + } - writeValueArray( file, inkle ); - } - while ( count != 0 ); + // Open the source before truncating the target, so that a failure here + // leaves the target intact. + std::ifstream source = MDAL::openInputFile( tmp, std::ios_base::in | std::ios_base::binary ); + if ( !source.is_open() ) + { + error = "Unable to read file " + tmp; + MDAL::deleteFile( tmp ); + return false; + } + std::ofstream destination = MDAL::openOutputFile( target, std::ofstream::binary ); + if ( !destination.is_open() ) + { + error = "Unable to replace file " + target; + source.close(); // Windows refuses to delete a file that is still open + MDAL::deleteFile( tmp ); + return false; } - writeInt( file, MDAL::toInt( facesCount * verticesPerFace * 4 ) ); - // IPOBO filled with 0 - writeValueArrayRecord( file, std::vector( verticesCount, 0 ) ); + const size_t copyBufferSize = 65536; + std::vector buffer( copyBufferSize ); + while ( source.read( buffer.data(), copyBufferSize ) || source.gcount() > 0 ) + destination.write( buffer.data(), source.gcount() ); + destination.flush(); + const bool copied = destination.good() && !source.bad(); + destination.close(); + source.close(); // Windows refuses to delete a file that is still open + if ( !copied || destination.fail() ) + { + error = "Unable to replace file " + target + ", new content kept as " + tmp; + return false; + } - //Vertices - writeVertices( file, mesh ); + MDAL::deleteFile( tmp ); + return true; +} - file.close(); +void MDAL::DriverSelafin::save( const std::string &fileName, const std::string &, MDAL::Mesh *mesh ) +{ + // Write to a temporary file first: a MeshSelafin's frame is read lazily + // from the source file during the save, which may be fileName itself. + const std::string tempFileName = fileName + ".tmp"; + // replaceFile() takes the temporary file over: once it has run, the catch + // blocks below must not remove it, it may hold the only copy of the mesh. + bool ownTempFile = true; + + try + { + // Read everything from the mesh BEFORE the output file is opened. + const SelafinMeshFrame frame = collectMeshFrame( mesh ); + + std::ofstream file = MDAL::openOutputFile( tempFileName, std::ofstream::binary ); + if ( !file.is_open() ) + throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, "Could not open file " + tempFileName ); + + writeMeshFrame( file, frame ); + file.flush(); + const bool written = file.good(); + file.close(); + // a full disk must fail the save here instead of replacing the target with + // a truncated file + if ( !written || file.fail() ) + throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, "Could not write file " + tempFileName ); + + std::string error; + ownTempFile = false; + if ( !replaceFile( tempFileName, fileName, error ) ) + throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, error ); + + // A MeshSelafin saved onto its own source now reads a file that no longer + // holds its datasets: release it so that the next access re-parses the + // file that has just been written. Mesh::uri() is the raw file name passed + // to SelafinFile::createMesh and MDAL has no path normalisation, so the + // same file spelled differently is not detected here; the reader then goes + // on serving the pre-save content, which is consistent but stale. + if ( mesh->uri() == fileName ) + mesh->closeSource(); + } + catch ( MDAL::Error &err ) + { + if ( ownTempFile ) + MDAL::deleteFile( tempFileName ); + MDAL::Log::error( err, name() ); + } + catch ( MDAL_Status status ) + { + if ( ownTempFile ) + MDAL::deleteFile( tempFileName ); + MDAL::Log::error( status, name(), "error occurred while saving mesh frame" ); + } } std::string MDAL::DriverSelafin::writeDatasetOnFileSuffix() const @@ -1283,18 +1475,24 @@ bool MDAL::SelafinFile::addDatasetGroup( MDAL::DatasetGroup *datasetGroup ) } } + out.flush(); + const bool written = out.good(); out.close(); mIn.close(); + // a full disk must fail here instead of replacing the file with a truncated one + if ( !written || out.fail() ) + { + MDAL::deleteFile( tempFileName ); + throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, "Unable to write dataset in file " + tempFileName ); + } // if the uri of the dataset group is the same than the file name, be sure to close it before replace it if ( datasetGroup->uri() == mFileName ) datasetGroup->mesh()->closeSource(); - if ( !MDAL::deleteFile( mFileName ) || !MDAL::renameFile( tempFileName, mFileName ) ) - { - MDAL::deleteFile( tempFileName ); - throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, "Unable to write dataset in file" ); - } + std::string error; + if ( !replaceFile( tempFileName, mFileName, error ) ) + throw MDAL::Error( MDAL_Status::Err_FailToWriteToDisk, error ); parseFile(); diff --git a/tests/test_selafin.cpp b/tests/test_selafin.cpp index 8343fb04..d11e8316 100644 --- a/tests/test_selafin.cpp +++ b/tests/test_selafin.cpp @@ -6,6 +6,7 @@ #include "gtest/gtest.h" #include #include +#include //mdal #include "mdal.h" @@ -276,6 +277,115 @@ TEST( MeshSLFTest, SaveMeshFrame ) "SELAFIN" ); } +static bool fileIsPresent( const std::string &fileName ) +{ + std::ifstream f( fileName, std::ios::binary ); + return f.is_open(); +} + +TEST( MeshSLFTest, TruncatedFileUnderOpenHandle ) +{ + // A Selafin mesh reads its frame and its values lazily, so a file rewritten + // or truncated by another program while MDAL holds it open fails in the + // middle of a read. Those reads happen under the C API, which has no + // exception handling of its own: they must report the failure and return + // nothing instead of terminating the calling process. + std::string file = tmp_file( "/selafin_truncated_under_handle.slf" ); + copy( test_file( "/slf/example_res_fr.slf" ), file ); + + MDAL_MeshH mesh = MDAL_LoadMesh( file.c_str() ); + ASSERT_NE( mesh, nullptr ); + ASSERT_EQ( MDAL_Status::None, MDAL_LastStatus() ); + MDAL_DatasetGroupH group = MDAL_M_datasetGroup( mesh, 0 ); + ASSERT_NE( group, nullptr ); + const int datasetCount = MDAL_G_datasetCount( group ); + ASSERT_GT( datasetCount, 0 ); + MDAL_DatasetH dataset = MDAL_G_dataset( group, datasetCount - 1 ); + ASSERT_NE( dataset, nullptr ); + + // truncate the file while the mesh handle is open + { + std::ofstream truncated( file, std::ios::binary | std::ios::trunc ); + ASSERT_TRUE( truncated.is_open() ); + } + + // the frame has been parsed already, so the counts stay available + EXPECT_GT( MDAL_M_vertexCount( mesh ), 0 ); + + MDAL_ResetStatus(); + std::vector coordinates( 3 * 10 ); + MDAL_MeshVertexIteratorH vertexIterator = MDAL_M_vertexIterator( mesh ); + EXPECT_EQ( 0, MDAL_VI_next( vertexIterator, 10, coordinates.data() ) ); + EXPECT_NE( MDAL_Status::None, MDAL_LastStatus() ); + MDAL_VI_close( vertexIterator ); + + MDAL_ResetStatus(); + std::vector faceOffsets( 10 ); + std::vector vertexIndices( 30 ); + MDAL_MeshFaceIteratorH faceIterator = MDAL_M_faceIterator( mesh ); + EXPECT_EQ( 0, MDAL_FI_next( faceIterator, 10, faceOffsets.data(), 30, vertexIndices.data() ) ); + EXPECT_NE( MDAL_Status::None, MDAL_LastStatus() ); + MDAL_FI_close( faceIterator ); + + MDAL_ResetStatus(); + std::vector values( 2 * 10 ); + const MDAL_DataType dataType = MDAL_G_hasScalarData( group ) ? + MDAL_DataType::SCALAR_DOUBLE : MDAL_DataType::VECTOR_2D_DOUBLE; + EXPECT_EQ( 0, MDAL_D_data( dataset, 0, 10, dataType, values.data() ) ); + EXPECT_NE( MDAL_Status::None, MDAL_LastStatus() ); + + MDAL_CloseMesh( mesh ); +} + +TEST( MeshSLFTest, SaveMeshOntoItsOwnFile ) +{ + // What QGIS does when an edited mesh layer is saved: the mesh is written to + // the very file the layer is still reading. The save must succeed, the file + // must remain a valid mesh, and the handle that is still open must keep + // working - it now reads the file that has just been written, which holds + // the frame but no dataset any more. + std::string file = tmp_file( "/selafin_save_onto_itself.slf" ); + copy( test_file( "/slf/example_res_fr.slf" ), file ); + + MDAL_MeshH mesh = MDAL_LoadMesh( file.c_str() ); + ASSERT_NE( mesh, nullptr ); + const int vertexCount = MDAL_M_vertexCount( mesh ); + const int faceCount = MDAL_M_faceCount( mesh ); + ASSERT_GT( vertexCount, 0 ); + ASSERT_GT( faceCount, 0 ); + MDAL_DatasetGroupH group = MDAL_M_datasetGroup( mesh, 0 ); + ASSERT_NE( group, nullptr ); + MDAL_DatasetH dataset = MDAL_G_dataset( group, 0 ); + ASSERT_NE( dataset, nullptr ); + + MDAL_SaveMesh( mesh, file.c_str(), "SELAFIN" ); + EXPECT_EQ( MDAL_Status::None, MDAL_LastStatus() ); + EXPECT_FALSE( fileIsPresent( file + ".tmp" ) ) << "no temporary file must be left behind"; + + // the frame is still served on the open handle + EXPECT_EQ( vertexCount, MDAL_M_vertexCount( mesh ) ); + EXPECT_EQ( faceCount, MDAL_M_faceCount( mesh ) ); + + // the datasets are not in the file any more: reading them reports an error + // instead of terminating the process + MDAL_ResetStatus(); + std::vector values( 2 * 10 ); + const MDAL_DataType dataType = MDAL_G_hasScalarData( group ) ? + MDAL_DataType::SCALAR_DOUBLE : MDAL_DataType::VECTOR_2D_DOUBLE; + EXPECT_EQ( 0, MDAL_D_data( dataset, 0, 10, dataType, values.data() ) ); + EXPECT_NE( MDAL_Status::None, MDAL_LastStatus() ); + MDAL_CloseMesh( mesh ); + + // and the file on disk is a valid frame-only mesh + MDAL_MeshH reloaded = MDAL_LoadMesh( file.c_str() ); + ASSERT_NE( reloaded, nullptr ); + EXPECT_EQ( MDAL_Status::None, MDAL_LastStatus() ); + EXPECT_EQ( vertexCount, MDAL_M_vertexCount( reloaded ) ); + EXPECT_EQ( faceCount, MDAL_M_faceCount( reloaded ) ); + EXPECT_EQ( 0, MDAL_M_datasetGroupCount( reloaded ) ); + MDAL_CloseMesh( reloaded ); +} + static MDAL_DatasetGroupH addNewScalarDatasetGroup( MDAL_MeshH mesh, MDAL_DriverH driver, std::string file ) { MDAL_DatasetGroupH newScalarGroup = MDAL_M_addDatasetGroup( mesh, "New Scalar Dataset Group", DataOnVertices, true, driver, file.c_str() );