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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 */
22
23// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24467.
24// CollectFilesLoopSafe / CollectSubdirsLoopSafe must terminate on recursive
25// symlinks, refuse to escape the scan root, and still resolve legitimate
26// one-level symlinks.
27
28#include <filesystem>
29#include <fstream>
30#include <system_error>
31
33#include <boost/test/unit_test.hpp>
34
35#include <gestfich.h>
36
37#include <wx/arrstr.h>
38#include <wx/filename.h>
39#include <wx/utils.h>
40
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
242 std::ofstream( scratch.path / "R_0603.step" ) << "ISO-10303-21;\n";
243 std::ofstream( scratch.path / ".hidden.step" ) << "ISO-10303-21;\n";
244
245 wxArrayString withHidden;
246 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), withHidden );
247
248 BOOST_CHECK( contains( withHidden, scratch.path / "R_0603.step" ) );
249 BOOST_CHECK( contains( withHidden, scratch.path / ".hidden.step" ) );
250
251 wxArrayString noHidden;
252 CollectFilesLoopSafe( wxString::FromUTF8( scratch.path.string() ), noHidden, wxEmptyString,
253 wxDIR_FILES | wxDIR_DIRS );
254
255 BOOST_CHECK( contains( noHidden, scratch.path / "R_0603.step" ) );
256 BOOST_CHECK( !contains( noHidden, scratch.path / ".hidden.step" ) );
257}
258
259
260// A one-level child symlink pointing at an external (sibling) directory is
261// intentionally followed -- this is how users alias shared library trees. The
262// loop guard only blocks re-entry and root escape (`-> /` or an ancestor of the
263// scan root), not bounded targets that simply live elsewhere.
264BOOST_AUTO_TEST_CASE( ChildSymlinkToExternalDirIsFollowed )
265{
266 namespace fs = std::filesystem;
267
268 const ScratchRoot scratch( "external" );
269 const fs::path root = scratch.path / "scan";
270 const fs::path external = scratch.path / "external_lib";
271 const fs::path externalFile = external / "C_0402.step";
272
273 std::error_code ec;
274 fs::create_directories( root, ec );
275 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
276 fs::create_directories( external, ec );
277 BOOST_REQUIRE_MESSAGE( !ec, ec.message() );
278
279 std::ofstream( externalFile ) << "ISO-10303-21;\n";
280
281 if( !createSymlinkOrSkip( external, root / "alias" ) )
282 return;
283
284 wxArrayString files;
285 CollectFilesLoopSafe( wxString::FromUTF8( root.string() ), files );
286
287 BOOST_CHECK( contains( files, root / "alias" / "C_0402.step" ) );
288}
289
290
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:831
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:821
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:100
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")