KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_glb_utils.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <cstring>
27#include <filesystem>
28#include <fstream>
29#include <vector>
30
31#include <json_common.h>
32#include <wx/filename.h>
33
35
36
37static constexpr uint32_t GLB_MAGIC = 0x46546C67;
38static constexpr uint32_t GLB_CHUNK_JSON = 0x4E4F534A;
39static constexpr int GLTF_LINES_MODE = 1;
40static constexpr int GLTF_TRIANGLES_MODE = 4;
41
42
44{
45 int linesCount = 0;
48 bool valid = false;
49};
50
51
52static GLB_VALIDATION ValidateGlbFile( const wxString& aPath )
53{
55
56 std::ifstream inFile( aPath.ToStdString(), std::ios::binary );
57
58 if( !inFile )
59 return result;
60
61 std::vector<uint8_t> data( ( std::istreambuf_iterator<char>( inFile ) ),
62 std::istreambuf_iterator<char>() );
63 inFile.close();
64
65 if( data.size() < 20 )
66 return result;
67
68 uint32_t magic = 0;
69 uint32_t jsonLen = 0;
70 uint32_t jsonType = 0;
71 std::memcpy( &magic, &data[0], 4 );
72 std::memcpy( &jsonLen, &data[12], 4 );
73 std::memcpy( &jsonType, &data[16], 4 );
74
75 if( magic != GLB_MAGIC || jsonType != GLB_CHUNK_JSON )
76 return result;
77
78 if( 20 + jsonLen > data.size() )
79 return result;
80
81 std::string jsonStr( data.begin() + 20, data.begin() + 20 + jsonLen );
82
83 nlohmann::json gltf;
84
85 try
86 {
87 gltf = nlohmann::json::parse( jsonStr );
88 }
89 catch( const nlohmann::json::parse_error& )
90 {
91 return result;
92 }
93
94 result.valid = true;
95
96 if( !gltf.contains( "meshes" ) || !gltf.contains( "accessors" ) )
97 return result;
98
99 auto& accessors = gltf["accessors"];
100
101 for( auto& mesh : gltf["meshes"] )
102 {
103 if( !mesh.contains( "primitives" ) )
104 continue;
105
106 for( auto& prim : mesh["primitives"] )
107 {
108 int mode = prim.value( "mode", GLTF_TRIANGLES_MODE );
109
110 if( mode == GLTF_TRIANGLES_MODE )
111 {
112 result.trianglesCount++;
113 }
114 else if( mode == GLTF_LINES_MODE )
115 {
116 result.linesCount++;
117
118 if( prim.contains( "indices" ) )
119 {
120 int idx = prim["indices"].get<int>();
121
122 if( idx >= 0 && idx < static_cast<int>( accessors.size() ) )
123 {
124 int count = accessors[idx].value( "count", 0 );
125
126 if( count % 2 != 0 )
127 result.oddLinesCount++;
128 }
129 }
130 }
131 }
132 }
133
134 return result;
135}
136
137
138static wxString GetTestGlbPath( const wxString& aFilename )
139{
140 return wxString( KI_TEST::GetTestDataRootDir() ) + wxS( "common/issue21706/" ) + aFilename;
141}
142
143
144// Serialize a glTF document into a minimal GLB (header + JSON chunk + empty BIN chunk) and
145// write it to a unique temp file. Lets the edge-case tests exercise paths the captured
146// fixtures cannot reach, such as a shared indices accessor or a single-index LINES primitive.
147static wxString WriteTempGlb( const nlohmann::json& aGltf, const wxString& aPrefix )
148{
149 std::string json = aGltf.dump();
150
151 while( json.size() % 4 != 0 )
152 json.push_back( ' ' );
153
154 uint32_t jsonLen = static_cast<uint32_t>( json.size() );
155 uint32_t version = 2;
156 uint32_t binLen = 0;
157 uint32_t binType = 0x004E4942; // "BIN\0" in little-endian
158 uint32_t total = 12 + 8 + jsonLen + 8 + binLen;
159
160 std::vector<uint8_t> data;
161
162 auto append32 = [&]( uint32_t v )
163 {
164 uint8_t bytes[4];
165 std::memcpy( bytes, &v, 4 );
166 data.insert( data.end(), bytes, bytes + 4 );
167 };
168
169 append32( GLB_MAGIC );
170 append32( version );
171 append32( total );
172 append32( jsonLen );
173 append32( GLB_CHUNK_JSON );
174 data.insert( data.end(), json.begin(), json.end() );
175 append32( binLen );
176 append32( binType );
177
178 wxString path( wxFileName::CreateTempFileName( aPrefix ) );
179 std::ofstream out( path.ToStdString(), std::ios::binary | std::ios::trunc );
180 out.write( reinterpret_cast<const char*>( data.data() ), data.size() );
181 out.close();
182
183 return path;
184}
185
186
187BOOST_AUTO_TEST_SUITE( GlbUtils )
188
189
190BOOST_AUTO_TEST_CASE( BadGlbHasOddLinesCount )
191{
192 wxString badPath = GetTestGlbPath( wxS( "issue21706 (BAD 9.0.4).glb" ) );
193
194 BOOST_REQUIRE( wxFileExists( badPath ) );
195
196 GLB_VALIDATION v = ValidateGlbFile( badPath );
197 BOOST_REQUIRE( v.valid );
200 BOOST_CHECK_GT( v.trianglesCount, 0 );
201}
202
203
204BOOST_AUTO_TEST_CASE( GoodGlbHasNoOddLinesCount )
205{
206 wxString goodPath = GetTestGlbPath( wxS( "issue21706 (GOOD 9.0.1).glb" ) );
207
208 BOOST_REQUIRE( wxFileExists( goodPath ) );
209
210 GLB_VALIDATION v = ValidateGlbFile( goodPath );
211 BOOST_REQUIRE( v.valid );
214 BOOST_CHECK_GT( v.trianglesCount, 0 );
215}
216
217
218BOOST_AUTO_TEST_CASE( FixGlbCorrectsBadFile )
219{
220 wxString badPath = GetTestGlbPath( wxS( "issue21706 (BAD 9.0.4).glb" ) );
221
222 BOOST_REQUIRE( wxFileExists( badPath ) );
223
224 // Copy to a unique temp file to avoid modifying the test data
225 std::filesystem::path tempFile( wxFileName::CreateTempFileName( wxS( "test_issue21706_fix" ) ).ToStdString() );
226 std::filesystem::copy_file( badPath.ToStdString(), tempFile,
227 std::filesystem::copy_options::overwrite_existing );
228
229 wxString tempPath( tempFile.string() );
230
231 // Verify the copy has the problem
232 GLB_VALIDATION before = ValidateGlbFile( tempPath );
233 BOOST_REQUIRE( before.valid );
234 BOOST_CHECK_EQUAL( before.oddLinesCount, 1 );
235
236 // Apply the fix
237 BOOST_CHECK( FixGlbLinesPrimitives( tempPath ) );
238
239 // Verify the fix was applied
240 GLB_VALIDATION after = ValidateGlbFile( tempPath );
241 BOOST_REQUIRE( after.valid );
243 BOOST_CHECK_EQUAL( after.linesCount, 1 );
245
246 std::filesystem::remove( tempFile );
247}
248
249
250BOOST_AUTO_TEST_CASE( FixGlbDoesNotModifyGoodFile )
251{
252 wxString goodPath = GetTestGlbPath( wxS( "issue21706 (GOOD 9.0.1).glb" ) );
253
254 BOOST_REQUIRE( wxFileExists( goodPath ) );
255
256 std::filesystem::path tempFile( wxFileName::CreateTempFileName( wxS( "test_issue21706_good" ) ).ToStdString() );
257 std::filesystem::copy_file( goodPath.ToStdString(), tempFile,
258 std::filesystem::copy_options::overwrite_existing );
259
260 wxString tempPath( tempFile.string() );
261
262 // Get original file size
263 auto origSize = std::filesystem::file_size( tempFile );
264
265 // Apply the fix (should be a no-op)
266 BOOST_CHECK( FixGlbLinesPrimitives( tempPath ) );
267
268 // Verify file size is unchanged (no modification occurred since there were no LINES)
269 auto newSize = std::filesystem::file_size( tempFile );
270 BOOST_CHECK_EQUAL( origSize, newSize );
271
272 // Verify still valid
273 GLB_VALIDATION after = ValidateGlbFile( tempPath );
274 BOOST_REQUIRE( after.valid );
276
277 std::filesystem::remove( tempFile );
278}
279
280
281BOOST_AUTO_TEST_CASE( FixGlbClonesSharedAccessor )
282{
283 // Two LINES primitives reference the same odd-count indices accessor. The fix must shorten
284 // one independently of the other instead of corrupting the shared accessor in place.
285 nlohmann::json gltf;
286 gltf["accessors"] = nlohmann::json::array(
287 { { { "count", 5 }, { "componentType", 5123 }, { "type", "SCALAR" } } } );
288 gltf["meshes"] = nlohmann::json::array(
289 { { { "primitives",
290 { { { "mode", GLTF_LINES_MODE }, { "indices", 0 } },
291 { { "mode", GLTF_LINES_MODE }, { "indices", 0 } } } } } } );
292
293 wxString path = WriteTempGlb( gltf, wxS( "test_glb_shared" ) );
294
295 BOOST_CHECK( FixGlbLinesPrimitives( path ) );
296
298 BOOST_REQUIRE( after.valid );
299 BOOST_CHECK_EQUAL( after.linesCount, 2 );
301
302 std::filesystem::remove( path.ToStdString() );
303}
304
305
306BOOST_AUTO_TEST_CASE( FixGlbDropsSingleIndexLine )
307{
308 // A LINES primitive with a single index forms no segment and is unfixable by trimming, so
309 // it must be dropped. A sibling triangles primitive keeps the mesh non-empty as the spec
310 // requires.
311 nlohmann::json gltf;
312 gltf["accessors"] = nlohmann::json::array(
313 { { { "count", 1 }, { "componentType", 5123 }, { "type", "SCALAR" } },
314 { { "count", 3 }, { "componentType", 5123 }, { "type", "SCALAR" } } } );
315 gltf["meshes"] = nlohmann::json::array(
316 { { { "primitives",
317 { { { "mode", GLTF_LINES_MODE }, { "indices", 0 } },
318 { { "mode", GLTF_TRIANGLES_MODE }, { "indices", 1 } } } } } } );
319
320 wxString path = WriteTempGlb( gltf, wxS( "test_glb_single" ) );
321
322 BOOST_CHECK( FixGlbLinesPrimitives( path ) );
323
325 BOOST_REQUIRE( after.valid );
326 BOOST_CHECK_EQUAL( after.linesCount, 0 );
329
330 std::filesystem::remove( path.ToStdString() );
331}
332
333
334BOOST_AUTO_TEST_CASE( FixGlbBailsOnLoneSingleIndexLine )
335{
336 // Dropping the only primitive would leave an empty, spec-invalid mesh. The fix must refuse
337 // and report failure rather than emit such a file.
338 nlohmann::json gltf;
339 gltf["accessors"] = nlohmann::json::array(
340 { { { "count", 1 }, { "componentType", 5123 }, { "type", "SCALAR" } } } );
341 gltf["meshes"] = nlohmann::json::array(
342 { { { "primitives",
343 { { { "mode", GLTF_LINES_MODE }, { "indices", 0 } } } } } } );
344
345 wxString path = WriteTempGlb( gltf, wxS( "test_glb_lone" ) );
346
347 BOOST_CHECK( !FixGlbLinesPrimitives( path ) );
348
349 std::filesystem::remove( path.ToStdString() );
350}
351
352
353BOOST_AUTO_TEST_CASE( FixGlbRejectsTruncatedFile )
354{
355 // A declared length exceeding the actual file marks a truncated GLB; the fix must refuse it
356 // instead of reading past the buffer or rewriting garbage.
357 nlohmann::json gltf;
358 gltf["accessors"] = nlohmann::json::array(
359 { { { "count", 3 }, { "componentType", 5123 }, { "type", "SCALAR" } } } );
360 gltf["meshes"] = nlohmann::json::array(
361 { { { "primitives",
362 { { { "mode", GLTF_LINES_MODE }, { "indices", 0 } } } } } } );
363
364 wxString path = WriteTempGlb( gltf, wxS( "test_glb_trunc" ) );
365
366 std::vector<uint8_t> data;
367 {
368 std::ifstream in( path.ToStdString(), std::ios::binary );
369 data.assign( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
370 }
371
372 uint32_t bogusTotal = static_cast<uint32_t>( data.size() ) + 1024;
373 std::memcpy( &data[8], &bogusTotal, 4 );
374
375 {
376 std::ofstream out( path.ToStdString(), std::ios::binary | std::ios::trunc );
377 out.write( reinterpret_cast<const char*>( data.data() ), data.size() );
378 }
379
380 BOOST_CHECK( !FixGlbLinesPrimitives( path ) );
381
382 std::filesystem::remove( path.ToStdString() );
383}
384
385
static std::string ToStdString(const wxString &aStr)
nlohmann::json json
Definition gerbview.cpp:49
static constexpr uint32_t GLB_CHUNK_JSON
Definition glb_utils.cpp:38
static constexpr uint32_t GLB_MAGIC
Definition glb_utils.cpp:37
static constexpr int GLTF_LINES_MODE
Definition glb_utils.cpp:39
bool FixGlbLinesPrimitives(const wxString &aFilePath)
Fix LINES primitives in a GLB file that have odd index counts.
Definition glb_utils.cpp:42
std::string GetTestDataRootDir()
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(BadGlbHasOddLinesCount)
static GLB_VALIDATION ValidateGlbFile(const wxString &aPath)
static wxString GetTestGlbPath(const wxString &aFilename)
static wxString WriteTempGlb(const nlohmann::json &aGltf, const wxString &aPrefix)
static constexpr int GLTF_TRIANGLES_MODE
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")