KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_embedded_file_compress.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
22#include <random>
23
24#include <wx/wfstream.h>
25
26#include <qa_utils/file_utils.h>
27
28#include <magic_enum.hpp>
29#include <magic_enum_iostream.hpp>
30
31#include <mmh3_hash.h>
32#include <embedded_files.h>
33
34
35using magic_enum::iostream_operators::operator<<;
36
37BOOST_AUTO_TEST_SUITE( EmbeddedFiles )
38
39BOOST_AUTO_TEST_CASE( CompressAndEncode_OK )
40{
42 file.name = "test_file";
43 std::string data = "Hello, World!";
44 file.decompressedData.assign(data.begin(), data.end());
45
47 hash.add( file.decompressedData );
48 file.data_hash = hash.digest().ToString();
49
52}
53
54BOOST_AUTO_TEST_CASE( DecompressAndDecode_OK )
55{
57 file.name = "test_file";
58 std::string data = "Hello, World!";
59 file.decompressedData.assign( data.begin(), data.end() );
60
62 hash.add( file.decompressedData );
63 file.data_hash = hash.digest().ToString();
64
67
70
71 // Create a large test data
72 data.clear();
73 data.reserve( 13 * 100000 + 1 );
74
75 for( int i = 0; i < 100000; ++i )
76 data += "Hello, World!";
77
78 file.decompressedData.assign( data.begin(), data.end() );
79
80 hash.reset();
81 hash.add( file.decompressedData );
82 file.data_hash = hash.digest().ToString();
83
86
89
90 // Create a sequential test dataset
91 data.clear();
92 data.reserve( 100000 );
93
94 for( int i = 0; i < 100000; ++i )
95 data += static_cast<char>( i % 256 );
96
97 file.decompressedData.assign( data.begin(), data.end() );
98 hash.reset();
99 hash.add( file.decompressedData );
100 file.data_hash = hash.digest().ToString();
101
104
107
108 // Create a random test dataset with a known seed
109 data.clear();
110 data.reserve( 100000 );
111
112 std::mt19937 rng;
113 rng.seed( 0 );
114
115 for( int i = 0; i < 100000; ++i )
116 data += static_cast<char>( rng() % 256 );
117
118 file.decompressedData.assign( data.begin(), data.end() );
119 hash.reset();
120 hash.add( file.decompressedData );
121 file.data_hash = hash.digest().ToString();
122
125
128
129}
130
131BOOST_AUTO_TEST_CASE( DecompressAndDecode_ChecksumError )
132{
134 file.name = "test_file";
135 std::string data = "Hello, World!";
136 file.decompressedData.assign(data.begin(), data.end());
137
140
141 // Modify the checksum
142 file.data_hash[0] = 'x';
143
146}
147
148
149BOOST_AUTO_TEST_CASE( ComputeFileHash_MatchesEmbeddedHash )
150{
151 // Create a temp file with known content
152 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_embed_test" );
153 wxFileName tempFile = tempDir.CreateChildFileStr( "test.txt" );
154 std::string data = "Test file content for hash computation";
155
156 {
157 wxFFileOutputStream out( tempFile.GetFullPath() );
158 BOOST_REQUIRE( out.IsOk() );
159 out.Write( data.data(), data.size() );
160 }
161
162 // Compute hash via ComputeFileHash
163 std::string computedHash;
166 BOOST_CHECK( !computedHash.empty() );
167
168 // Embed the same file and verify hashes match
169 EMBEDDED_FILES files;
170 EMBEDDED_FILES::EMBEDDED_FILE* embedded = files.AddFile( tempFile, false );
171 BOOST_REQUIRE( embedded != nullptr );
172 BOOST_CHECK_EQUAL( computedHash, embedded->data_hash );
173}
174
175
176BOOST_AUTO_TEST_CASE( ComputeFileHash_DifferentContent )
177{
178 // Create two temp files with different content
179 KI_TEST::SCOPED_TEMP_DIR tempDir( "kicad_embed_test_diff" );
180
181 wxFileName tempFile1 = tempDir.CreateChildFileStr( "file1.txt" );
182 wxFileName tempFile2 = tempDir.CreateChildFileStr( "file2.txt" );
183
184 std::string data1 = "Content version 1";
185 std::string data2 = "Content version 2";
186
187 {
188 wxFFileOutputStream out1( tempFile1.GetFullPath() );
189 BOOST_REQUIRE( out1.IsOk() );
190 out1.Write( data1.data(), data1.size() );
191 }
192
193 {
194 wxFFileOutputStream out2( tempFile2.GetFullPath() );
195 BOOST_REQUIRE( out2.IsOk() );
196 out2.Write( data2.data(), data2.size() );
197 }
198
199 // Compute hashes for both files
200 std::string hash1, hash2;
205
206 // Hashes should be different
207 BOOST_CHECK_NE( hash1, hash2 );
208}
209
210
211BOOST_AUTO_TEST_CASE( ComputeFileHash_NonExistentFile )
212{
213 wxFileName nonExistent( wxS( "/nonexistent/path/file.txt" ) );
214 std::string hash;
215
218 BOOST_CHECK( hash.empty() );
219}
220
221
222BOOST_AUTO_TEST_CASE( DecompressAndDecode_V1HashFallback )
223{
224 // Verify that files hashed with the old (V1) MMH3 algorithm can still be
225 // decoded. The V1 algorithm had a tail-byte padding bug that inflated len
226 // and changed the hash for data sizes not a multiple of 16.
227 auto testV1Fallback = []( const std::string& aData )
228 {
230 file.name = "v1_hash_test";
231 file.decompressedData.assign( aData.begin(), aData.end() );
232
233 // Hash with V1 algorithm
235 v1hash.addDataV1( reinterpret_cast<const uint8_t*>( aData.data() ), aData.size() );
236 file.data_hash = v1hash.digest().ToString();
237
238 // Verify V1 and current hashes differ for non-aligned data
239 MMH3_HASH curHash( EMBEDDED_FILES::Seed() );
240 curHash.add( file.decompressedData );
241 std::string currentHash = curHash.digest().ToString();
242
243 if( aData.size() % 16 != 0 )
244 BOOST_CHECK_NE( file.data_hash, currentHash );
245
248
249 // DecompressAndDecode should succeed via V1 fallback
252
253 // After successful decode, hash should be migrated to current format
254 BOOST_CHECK_EQUAL( file.data_hash, currentHash );
255 };
256
257 // 13 bytes (remaining=13, old padding rounds to 16, hashTail sees 0 tail bytes)
258 testV1Fallback( "Hello, World!" );
259
260 // 17 bytes (remaining=1, old padding rounds to 4)
261 testV1Fallback( "12345678901234567" );
262
263 // 31 bytes (remaining=15, old padding rounds to 16)
264 testV1Fallback( "1234567890123456789012345678901" );
265
266 // 100000 bytes (remaining=16 mod 16 = 0, no tail, hashes should match)
267 std::string aligned( 100000UL * 16, 'x' );
269 alignedFile.name = "aligned_test";
270 alignedFile.decompressedData.assign( aligned.begin(), aligned.end() );
271
273 v1h.addDataV1( reinterpret_cast<const uint8_t*>( aligned.data() ), aligned.size() );
274 std::string v1Hash = v1h.digest().ToString();
275
277 curH.add( alignedFile.decompressedData );
278 std::string curHash = curH.digest().ToString();
279
280 BOOST_CHECK_EQUAL( v1Hash, curHash );
281}
282
283
284// Regression test for GitLab issue #24345. Copying an EMBEDDED_FILES collection must not
285// duplicate the underlying file payloads; multiple copies should share ownership through
286// shared_ptr so that cloning a footprint with embedded 3D models for an undo snapshot does
287// not multiply memory usage by the number of clones.
288BOOST_AUTO_TEST_CASE( CopySharesEmbeddedFilePayloads )
289{
290 EMBEDDED_FILES original;
291
292 auto* file = new EMBEDDED_FILES::EMBEDDED_FILE();
293 file->name = wxS( "shared_model.step" );
295
296 std::string payload( 1024 * 1024, 'x' );
297 file->decompressedData.assign( payload.begin(), payload.end() );
298
300 hash.add( file->decompressedData );
301 file->data_hash = hash.digest().ToString();
302
303 BOOST_REQUIRE_EQUAL( EMBEDDED_FILES::CompressAndEncode( *file ),
305
306 original.AddFile( file );
307
308 EMBEDDED_FILES::EMBEDDED_FILE* originalFile = original.GetEmbeddedFile( wxS( "shared_model.step" ) );
309 BOOST_REQUIRE( originalFile );
310
311 // Copy via copy constructor; sharing means both pointers reference the same payload.
312 EMBEDDED_FILES copy1( original );
313 EMBEDDED_FILES::EMBEDDED_FILE* copy1File = copy1.GetEmbeddedFile( wxS( "shared_model.step" ) );
314 BOOST_REQUIRE( copy1File );
315 BOOST_CHECK_EQUAL( copy1File, originalFile );
316
317 // Copy via assignment operator deep-copies (assignment targets a live object that may
318 // later mutate file fields through raw pointers; aliasing would silently bleed mutations
319 // back into the source).
320 EMBEDDED_FILES copy2;
321 copy2 = original;
322 EMBEDDED_FILES::EMBEDDED_FILE* copy2File = copy2.GetEmbeddedFile( wxS( "shared_model.step" ) );
323 BOOST_REQUIRE( copy2File );
324 BOOST_CHECK_NE( copy2File, originalFile );
325 BOOST_CHECK_EQUAL( copy2File->data_hash, originalFile->data_hash );
326
327 // Destroying the original must keep the payload alive via the surviving copies.
328 {
329 EMBEDDED_FILES transient( original );
330 transient.ClearEmbeddedFiles();
331 BOOST_CHECK( !transient.HasFile( wxS( "shared_model.step" ) ) );
332 }
333
334 BOOST_CHECK( original.HasFile( wxS( "shared_model.step" ) ) );
335 BOOST_CHECK( copy1.HasFile( wxS( "shared_model.step" ) ) );
336 BOOST_CHECK( copy2.HasFile( wxS( "shared_model.step" ) ) );
337
338 // Validate the payload via one of the shared copies (round-trip).
341 BOOST_CHECK_EQUAL( std::string( copy1File->decompressedData.begin(),
342 copy1File->decompressedData.end() ),
343 payload );
344}
345
346
347// Explicit deep-copy form must allocate independent EMBEDDED_FILE objects.
348BOOST_AUTO_TEST_CASE( DeepCopyAllocatesIndependentPayloads )
349{
350 EMBEDDED_FILES original;
351
352 auto* file = new EMBEDDED_FILES::EMBEDDED_FILE();
353 file->name = wxS( "deep_copy.bin" );
354 file->decompressedData.assign( { '1', '2', '3' } );
355
357 hash.add( file->decompressedData );
358 file->data_hash = hash.digest().ToString();
359
360 BOOST_REQUIRE_EQUAL( EMBEDDED_FILES::CompressAndEncode( *file ),
362
363 original.AddFile( file );
364
365 EMBEDDED_FILES deep( original, /*aDeepCopy=*/true );
366 EMBEDDED_FILES::EMBEDDED_FILE* deepFile = deep.GetEmbeddedFile( wxS( "deep_copy.bin" ) );
367
368 BOOST_REQUIRE( deepFile );
369 BOOST_CHECK_NE( deepFile, file );
370 BOOST_CHECK_EQUAL( deepFile->data_hash, file->data_hash );
371}
372
373
374// A paged setup dialog commits its working copy on both page change and OK, so the commit must
375// leave the source intact. A destructive commit wiped the board/schematic on the second pass and
376// silently dropped the embedded drawing sheet (issue 24998).
377BOOST_AUTO_TEST_CASE( AssignSharedFromIsIdempotent )
378{
379 EMBEDDED_FILES working;
380
381 auto* file = new EMBEDDED_FILES::EMBEDDED_FILE();
382 file->name = wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" );
384 file->decompressedData.assign( { '1', '2', '3' } );
385
387 hash.add( file->decompressedData );
388 file->data_hash = hash.digest().ToString();
389
390 BOOST_REQUIRE_EQUAL( EMBEDDED_FILES::CompressAndEncode( *file ),
392
393 working.AddFile( file );
394
395 EMBEDDED_FILES target;
396
397 target.AssignSharedFrom( working );
398 target.AssignSharedFrom( working );
399
400 // IsEmpty() gates whether the embedded_files block is written on save.
401 BOOST_CHECK( !target.IsEmpty() );
402 BOOST_CHECK( target.HasFile( wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" ) ) );
403 BOOST_CHECK( working.HasFile( wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" ) ) );
404
405 // Self-assignment must not empty the collection.
406 working.AssignSharedFrom( working );
407 BOOST_CHECK( working.HasFile( wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" ) ) );
408
409 std::set<wxString> exclude{ wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" ) };
410 working.AssignSharedFrom( working, exclude );
411 BOOST_CHECK( !working.HasFile( wxS( "ARTIDIS-KiCAD_HEADER.kicad_wks" ) ) );
412}
413
414
415// AddFile() destroys a name-duplicate, so it must hand back the entry the collection holds;
416// callers that kept using their own pointer were reading freed memory (issue 25362)
417BOOST_AUTO_TEST_CASE( AddFileReturnsTheStoredEntry )
418{
419 EMBEDDED_FILES files;
420
421 auto* first = new EMBEDDED_FILES::EMBEDDED_FILE();
422 first->name = wxS( "duplicate.step" );
423 first->decompressedData.assign( { 'k', 'e', 'p', 't' } );
424
425 BOOST_REQUIRE_EQUAL( files.AddFile( first ), first );
426
428 duplicate->name = wxS( "duplicate.step" );
429 duplicate->decompressedData.assign( { 'd', 'r', 'o', 'p', 'p', 'e', 'd' } );
430
432
433 BOOST_REQUIRE_EQUAL( stored, first );
434 BOOST_CHECK_EQUAL( files.EmbeddedFileMap().size(), 1 );
435
436 // The returned pointer must stay usable; the caller's own is already gone
437 BOOST_REQUIRE_EQUAL( EMBEDDED_FILES::CompressAndEncode( *stored ),
439 BOOST_CHECK( stored->Validate() );
440 BOOST_CHECK_EQUAL( std::string( stored->decompressedData.begin(),
441 stored->decompressedData.end() ),
442 std::string( "kept" ) );
443}
444
445
bool IsEmpty() const
static RETURN_CODE DecompressAndDecode(EMBEDDED_FILE &aFile, bool aAllowEmptyHash=false)
Takes data from the #compressedEncodedData buffer and Base64 decodes it.
@ FILE_NOT_FOUND
File not found on disk.
@ CHECKSUM_ERROR
Checksum in file does not match data.
EMBEDDED_FILE * GetEmbeddedFile(const wxString &aName) const
Returns the embedded file with the given name or nullptr if it does not exist.
static RETURN_CODE ComputeFileHash(const wxFileName &aFileName, std::string &aHash)
Compute the hash of a file on disk without fully embedding it.
bool HasFile(const wxString &name) const
void ClearEmbeddedFiles(bool aDeleteFiles=true)
EMBEDDED_FILE * AddFile(const wxFileName &aName, bool aOverwrite)
Load a file from disk and adds it to the collection.
static uint32_t Seed()
const std::map< wxString, std::shared_ptr< EMBEDDED_FILE > > & EmbeddedFileMap() const
Provide an iterable view of the file collection.
static RETURN_CODE CompressAndEncode(EMBEDDED_FILE &aFile)
Take data from the #decompressedData buffer and compresses it using ZSTD into the #compressedEncodedD...
void AssignSharedFrom(const EMBEDDED_FILES &aSource, const std::set< wxString > &aExcludedNames={})
Replace this collection's files with references to aSource's files, skipping any whose name appears i...
wxString CreateChildFileStr(const wxString &aName) const
Create and return the path to a direct child file as a wxString.
A streaming C++ equivalent for MurmurHash3_x64_128.
Definition mmh3_hash.h:56
FORCE_INLINE void addDataV1(const uint8_t *data, size_t length)
Definition mmh3_hash.h:95
FORCE_INLINE void add(const std::string &input)
Definition mmh3_hash.h:117
FORCE_INLINE HASH_128 digest()
Definition mmh3_hash.h:136
FORCE_INLINE void reset(uint32_t aSeed=0)
Definition mmh3_hash.h:62
static thread_local boost::mt19937 rng
Definition kiid.cpp:49
std::vector< char > decompressedData
std::string ToString() const
Definition hash_128.h:43
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(CompressAndEncode_OK)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")