KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_loop_safe_collect.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, see <https://www.gnu.org/licenses/>.
18 */
19
20// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24467.
21// CollectFilesLoopSafe / CollectSubdirsLoopSafe must terminate on recursive
22// symlinks, refuse to escape the scan root, and still resolve legitimate
23// one-level symlinks.
24
25#include <filesystem>
26#include <fstream>
27#include <system_error>
28
30#include <boost/test/unit_test.hpp>
31
32#include <gestfich.h>
33
34#include <wx/arrstr.h>
35#include <wx/filename.h>
36#include <wx/utils.h>
37
38#if defined (__MINGW32__)
39 #include <fileapi.h>
40#endif
41
42namespace
43{
44
45// RAII so a mid-test BOOST_REQUIRE failure (or a thrown exception from
46// std::filesystem) can't leak a temp tree -- particularly important here
47// because the trees contain symlinks back into themselves.
48struct ScratchRoot
49{
50 std::filesystem::path path;
51
52 explicit ScratchRoot( const char* aTag )
53 {
54 namespace fs = std::filesystem;
55 std::error_code ec;
56 path = fs::temp_directory_path( ec );
57 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
58
59 path /= std::string( "kicad_qa_issue24467_" ) + aTag + "_"
60 + std::to_string( static_cast<unsigned long>( wxGetProcessId() ) );
61
62 fs::remove_all( path, ec );
63 fs::create_directories( path, ec );
64 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
65 }
66
67 ~ScratchRoot()
68 {
69 std::error_code ec;
70 std::filesystem::remove_all( path, ec );
71 }
72};
73
74
75bool contains( const wxArrayString& aFiles, const std::filesystem::path& aPath )
76{
77 const wxString want = wxString::FromUTF8( aPath.string() );
78
79 for( const wxString& f : aFiles )
80 {
81 if( f == want )
82 return true;
83 }
84
85 return false;
86}
87
88
89// Create a directory symlink aTarget <- aLink. Returns true on success; on a
90// platform that can't make symlinks it emits a skip message so the caller can
91// bail out of the test cleanly.
92bool createSymlinkOrSkip( const std::filesystem::path& aTarget,
93 const std::filesystem::path& aLink )
94{
95 std::error_code ec;
96 std::filesystem::create_directory_symlink( aTarget, aLink, ec );
97
98 if( ec )
99 BOOST_TEST_MESSAGE( "Skipping: symlink creation unsupported (" << ec.message() << ")" );
100
101 return !ec;
102}
103
104} // namespace
105
106
107BOOST_AUTO_TEST_SUITE( GestfichLoopSafeCollect )
108
109
110// A self-referencing symlink must not send CollectFilesLoopSafe into
111// unbounded recursion; the legitimate file alongside it must still be found.
112BOOST_AUTO_TEST_CASE( RecursiveSymlinkTerminates )
113{
114 namespace fs = std::filesystem;
115
116 const ScratchRoot scratch( "loop" );
117 const fs::path stepFile = scratch.path / "R_0603.step";
118
119 std::ofstream( stepFile ) << "ISO-10303-21;\n";
120
121 if( !createSymlinkOrSkip( ".", scratch.path / "loop" ) )
122 return;
123
124 wxArrayString files;
125 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), files );
126
127 BOOST_CHECK( contains( files, stepFile ) );
128}
129
130
131// A one-level symlink that points at a sibling directory containing files
132// must be followed so that legitimate user library aliases keep working.
133BOOST_AUTO_TEST_CASE( SingleSymlinkResolves )
134{
135 namespace fs = std::filesystem;
136
137 const ScratchRoot scratch( "follow" );
138 const fs::path realDir = scratch.path / "real";
139 const fs::path linkedDir = scratch.path / "scan";
140 const fs::path stepFile = realDir / "R_0603.step";
141
142 std::error_code ec;
143 fs::create_directories( realDir, ec );
144 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
145
146 std::ofstream( stepFile ) << "ISO-10303-21;\n";
147
148 if( !createSymlinkOrSkip( realDir, linkedDir ) )
149 return;
150
151 wxArrayString files;
152 CollectFilesLoopSafe( wxString::FromUTF8( linkedDir.string() ), files );
153
154 BOOST_CHECK_EQUAL( files.GetCount(), 1u );
155 BOOST_CHECK( contains( files, linkedDir / "R_0603.step" ) );
156}
157
158
159// A symlink whose target is an ancestor of the scan root (the shape of
160// Wine's `dosdevices/z: -> /`) must not escape the scan tree. Otherwise
161// scanning a Wine prefix would walk all of /.
162BOOST_AUTO_TEST_CASE( AncestorSymlinkDoesNotEscape )
163{
164 namespace fs = std::filesystem;
165
166 const ScratchRoot scratch( "ancestor" );
167 const fs::path root = scratch.path / "scan";
168 const fs::path outside = scratch.path / "outside";
169 const fs::path outsideFile = outside / "Outside.step";
170
171 std::error_code ec;
172 fs::create_directories( root, ec );
173 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
174 fs::create_directories( outside, ec );
175 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
176
177 std::ofstream( outsideFile ) << "ISO-10303-21;\n";
178
179 if( !createSymlinkOrSkip( scratch.path, root / "up" ) )
180 return;
181
182 wxArrayString files;
183 CollectFilesLoopSafe( wxString::FromUTF8( root.string() ), files );
184
185 BOOST_CHECK( !contains( files, root / "up" / "outside" / "Outside.step" ) );
186 BOOST_CHECK( !contains( files, outsideFile ) );
187}
188
189
190// A wildcard filter must restrict the collected files just as
191// wxDir::GetAllFiles() does, while still applying loop protection.
192BOOST_AUTO_TEST_CASE( FileSpecFiltersResults )
193{
194 namespace fs = std::filesystem;
195
196 const ScratchRoot scratch( "spec" );
197
198 std::ofstream( scratch.path / "R_0603.step" ) << "ISO-10303-21;\n";
199 std::ofstream( scratch.path / "R_0603.wrl" ) << "#VRML\n";
200
201 wxArrayString files;
202 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), files, wxS( "*.step" ) );
203
204 BOOST_CHECK_EQUAL( files.GetCount(), 1u );
205 BOOST_CHECK( contains( files, scratch.path / "R_0603.step" ) );
206}
207
208
209// CollectSubdirsLoopSafe must enumerate the real subdirectories and terminate
210// on a self-referencing symlink rather than recursing without bound.
211BOOST_AUTO_TEST_CASE( SubdirCollectionTerminates )
212{
213 namespace fs = std::filesystem;
214
215 const ScratchRoot scratch( "subdirs" );
216 const fs::path subDir = scratch.path / "Resistor_SMD.3dshapes";
217
218 std::error_code ec;
219 fs::create_directories( subDir, ec );
220 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
221
222 if( !createSymlinkOrSkip( ".", scratch.path / "loop" ) )
223 return;
224
225 wxArrayString dirs;
226 CollectSubdirsLoopSafe( wxString::FromUTF8( scratch.path.string() ), dirs );
227
228 BOOST_CHECK( contains( dirs, subDir ) );
229}
230
231
232// The hidden-entry behaviour must track wxDir::GetAllFiles(): the default flag
233// set includes dotfiles, while an explicit wxDIR_FILES | wxDIR_DIRS excludes
234// them. Footprint-library delete paths rely on the former; the 3D scan and the
235// sprint enumerator rely on the latter.
236BOOST_AUTO_TEST_CASE( HiddenFlagControlsDotfiles )
237{
238 namespace fs = std::filesystem;
239
240 const ScratchRoot scratch( "hidden" );
241 const fs::path normalFile = scratch.path / "R_0603.step";
242 const fs::path hiddenFile = scratch.path / ".hidden.step";
243
244 std::ofstream( normalFile ) << "ISO-10303-21;\n";
245 std::ofstream( hiddenFile ) << "ISO-10303-21;\n";
246
247#ifdef _WIN32
248 SetFileAttributesW( hiddenFile.c_str(), FILE_ATTRIBUTE_HIDDEN );
249#endif
250
251 wxArrayString withHidden;
252 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), withHidden );
253
254 BOOST_CHECK( contains( withHidden, normalFile ) );
255 BOOST_CHECK( contains( withHidden, hiddenFile ) );
256
257 wxArrayString noHidden;
258 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), noHidden, wxEmptyString,
259 wxDIR_FILES | wxDIR_DIRS );
260
261 BOOST_CHECK( contains( noHidden, normalFile ) );
262 BOOST_CHECK( !contains( noHidden, hiddenFile ) );
263}
264
265
266// A one-level child symlink pointing at an external (sibling) directory is
267// intentionally followed -- this is how users alias shared library trees. The
268// loop guard only blocks re-entry and root escape (`-> /` or an ancestor of the
269// scan root), not bounded targets that simply live elsewhere.
270BOOST_AUTO_TEST_CASE( ChildSymlinkToExternalDirIsFollowed )
271{
272 namespace fs = std::filesystem;
273
274 const ScratchRoot scratch( "external" );
275 const fs::path root = scratch.path / "scan";
276 const fs::path external = scratch.path / "external_lib";
277 const fs::path externalFile = external / "C_0402.step";
278
279 std::error_code ec;
280 fs::create_directories( root, ec );
281 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
282 fs::create_directories( external, ec );
283 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
284
285 std::ofstream( externalFile ) << "ISO-10303-21;\n";
286
287 if( !createSymlinkOrSkip( external, root / "alias" ) )
288 return;
289
290 wxArrayString files;
291 CollectFilesLoopSafe( wxString::FromUTF8( root.string() ), files );
292
293 BOOST_CHECK( contains( files, root / "alias" / "C_0402.step" ) );
294}
295
296
void CollectSubdirsLoopSafe(const wxString &aRoot, wxArrayString &aDirs, int aFlags)
Recursively collect every subdirectory under aRoot using the same loop detection as CollectFilesLoopS...
Definition gestfich.cpp:883
void CollectFilesLoopSafe(const wxString &aRoot, wxArrayString &aFiles, const wxString &aFileSpec, int aFlags)
Recursively collect every file under aRoot, deduplicating subdirectories by their resolved path.
Definition gestfich.cpp:873
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:96
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(RecursiveSymlinkTerminates)
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")