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