]>
Commit | Line | Data |
---|---|---|
641cb5a6 JW |
1 | // Class filesystem::directory_entry etc. -*- C++ -*- |
2 | ||
83ffe9cd | 3 | // Copyright (C) 2014-2023 Free Software Foundation, Inc. |
641cb5a6 JW |
4 | // |
5 | // This file is part of the GNU ISO C++ Library. This library is free | |
6 | // software; you can redistribute it and/or modify it under the | |
7 | // terms of the GNU General Public License as published by the | |
8 | // Free Software Foundation; either version 3, or (at your option) | |
9 | // any later version. | |
10 | ||
11 | // This library 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 | // Under Section 7 of GPL version 3, you are granted additional | |
17 | // permissions described in the GCC Runtime Library Exception, version | |
18 | // 3.1, as published by the Free Software Foundation. | |
19 | ||
20 | // You should have received a copy of the GNU General Public License and | |
21 | // a copy of the GCC Runtime Library Exception along with this program; | |
22 | // see the files COPYING3 and COPYING.RUNTIME respectively. If not, see | |
23 | // <http://www.gnu.org/licenses/>. | |
24 | ||
25 | #ifndef _GLIBCXX_USE_CXX11_ABI | |
26 | # define _GLIBCXX_USE_CXX11_ABI 1 | |
27 | #endif | |
28 | ||
2fc11587 | 29 | #include <bits/largefile-config.h> |
641cb5a6 | 30 | #include <filesystem> |
641cb5a6 JW |
31 | #include <utility> |
32 | #include <stack> | |
33 | #include <string.h> | |
34 | #include <errno.h> | |
35 | #define _GLIBCXX_BEGIN_NAMESPACE_FILESYSTEM namespace filesystem { | |
36 | #define _GLIBCXX_END_NAMESPACE_FILESYSTEM } | |
de4db54f | 37 | #include "../filesystem/dir-common.h" |
641cb5a6 JW |
38 | |
39 | namespace fs = std::filesystem; | |
9534a5e6 | 40 | namespace posix = std::filesystem::__gnu_posix; |
641cb5a6 | 41 | |
da29d2a3 JW |
42 | template class std::__shared_ptr<fs::_Dir>; |
43 | template class std::__shared_ptr<fs::recursive_directory_iterator::_Dir_stack>; | |
44 | ||
641cb5a6 JW |
45 | struct fs::_Dir : _Dir_base |
46 | { | |
c8bd4dc8 | 47 | _Dir(const fs::path& p, bool skip_permission_denied, bool nofollow, |
ebf61754 | 48 | [[maybe_unused]] bool filename_only, error_code& ec) |
19878114 | 49 | : _Dir_base(p.c_str(), skip_permission_denied, nofollow, ec) |
641cb5a6 | 50 | { |
835b1993 | 51 | #if _GLIBCXX_HAVE_DIRFD && _GLIBCXX_HAVE_OPENAT && _GLIBCXX_HAVE_UNLINKAT |
ebf61754 JW |
52 | if (filename_only) |
53 | return; // Do not store path p when we aren't going to use it. | |
54 | #endif | |
55 | ||
641cb5a6 JW |
56 | if (!ec) |
57 | path = p; | |
58 | } | |
59 | ||
1ef6085f | 60 | _Dir(_Dir_base&& d, const path& p) : _Dir_base(std::move(d)), path(p) { } |
641cb5a6 JW |
61 | |
62 | _Dir(_Dir&&) = default; | |
63 | ||
64 | // Returns false when the end of the directory entries is reached. | |
65 | // Reports errors by setting ec. | |
66 | bool advance(bool skip_permission_denied, error_code& ec) noexcept | |
67 | { | |
68 | if (const auto entp = _Dir_base::advance(skip_permission_denied, ec)) | |
69 | { | |
8d531548 JW |
70 | auto name = path; |
71 | name /= entp->d_name; | |
5ed5c0da JW |
72 | file_type type = file_type::none; |
73 | #ifdef _GLIBCXX_HAVE_STRUCT_DIRENT_D_TYPE | |
74 | // Even if the OS supports dirent::d_type the filesystem might not: | |
75 | if (entp->d_type != DT_UNKNOWN) | |
76 | type = get_file_type(*entp); | |
77 | #endif | |
78 | entry = fs::directory_entry{std::move(name), type}; | |
641cb5a6 JW |
79 | return true; |
80 | } | |
81 | else if (!ec) | |
82 | { | |
83 | // reached the end | |
84 | entry = {}; | |
85 | } | |
86 | return false; | |
87 | } | |
88 | ||
89 | bool advance(error_code& ec) noexcept { return advance(false, ec); } | |
90 | ||
91 | // Returns false when the end of the directory entries is reached. | |
92 | // Reports errors by throwing. | |
93 | bool advance(bool skip_permission_denied = false) | |
94 | { | |
95 | error_code ec; | |
96 | const bool ok = advance(skip_permission_denied, ec); | |
97 | if (ec) | |
98 | _GLIBCXX_THROW_OR_ABORT(filesystem_error( | |
99 | "directory iterator cannot advance", ec)); | |
100 | return ok; | |
101 | } | |
102 | ||
103 | bool should_recurse(bool follow_symlink, error_code& ec) const | |
104 | { | |
105 | file_type type = entry._M_type; | |
5ed5c0da | 106 | if (type == file_type::none) |
641cb5a6 JW |
107 | { |
108 | type = entry.symlink_status(ec).type(); | |
109 | if (ec) | |
110 | return false; | |
111 | } | |
112 | ||
113 | if (type == file_type::directory) | |
114 | return true; | |
115 | if (type == file_type::symlink) | |
116 | return follow_symlink && is_directory(entry.status(ec)); | |
117 | return false; | |
118 | } | |
119 | ||
19878114 JW |
120 | // Return a pathname for the current directory entry, as an _At_path. |
121 | _Dir_base::_At_path | |
122 | current() const noexcept | |
ebf61754 JW |
123 | { |
124 | const fs::path& p = entry.path(); | |
19878114 JW |
125 | #if _GLIBCXX_HAVE_DIRFD |
126 | if (!p.empty()) [[__likely__]] | |
127 | { | |
128 | auto len = std::prev(p.end())->native().size(); | |
129 | return {::dirfd(this->dirp), p.c_str(), p.native().size() - len}; | |
130 | } | |
ebf61754 | 131 | #endif |
19878114 | 132 | return p.c_str(); |
ebf61754 JW |
133 | } |
134 | ||
135 | // Create a new _Dir for the directory this->entry.path(). | |
136 | _Dir | |
137 | open_subdir(bool skip_permission_denied, bool nofollow, | |
138 | error_code& ec) const noexcept | |
139 | { | |
19878114 | 140 | _Dir_base d(current(), skip_permission_denied, nofollow, ec); |
ebf61754 JW |
141 | // If this->path is empty, the new _Dir should have an empty path too. |
142 | const fs::path& p = this->path.empty() ? this->path : this->entry.path(); | |
1ef6085f | 143 | return _Dir(std::move(d), p); |
ebf61754 JW |
144 | } |
145 | ||
146 | bool | |
147 | do_unlink(bool is_directory, error_code& ec) const noexcept | |
148 | { | |
149 | #if _GLIBCXX_HAVE_UNLINKAT | |
19878114 JW |
150 | const auto atp = current(); |
151 | if (::unlinkat(atp.dir(), atp.path_at_dir(), | |
152 | is_directory ? AT_REMOVEDIR : 0) == -1) | |
ebf61754 JW |
153 | { |
154 | ec.assign(errno, std::generic_category()); | |
155 | return false; | |
156 | } | |
157 | else | |
158 | { | |
159 | ec.clear(); | |
160 | return true; | |
161 | } | |
162 | #else | |
163 | return fs::remove(entry.path(), ec); | |
164 | #endif | |
165 | } | |
166 | ||
167 | // Remove the non-directory that this->entry refers to. | |
168 | bool | |
169 | unlink(error_code& ec) const noexcept | |
170 | { return do_unlink(/* is_directory*/ false, ec); } | |
171 | ||
172 | // Remove the directory that this->entry refers to. | |
173 | bool | |
174 | rmdir(error_code& ec) const noexcept | |
175 | { return do_unlink(/* is_directory*/ true, ec); } | |
176 | ||
177 | fs::path path; // Empty if only using unlinkat with file descr. | |
641cb5a6 JW |
178 | directory_entry entry; |
179 | }; | |
180 | ||
181 | namespace | |
182 | { | |
183 | template<typename Bitmask> | |
184 | inline bool | |
185 | is_set(Bitmask obj, Bitmask bits) | |
186 | { | |
187 | return (obj & bits) != Bitmask::none; | |
188 | } | |
ebf61754 JW |
189 | |
190 | // Non-standard directory option flags, currently only for internal use: | |
191 | // | |
192 | // Do not allow directory iterator to open a symlink. | |
193 | // This might seem redundant given directory_options::follow_directory_symlink | |
194 | // but that is only checked for recursing into sub-directories, and we need | |
195 | // something that controls the initial opendir() call in the constructor. | |
196 | constexpr fs::directory_options __directory_iterator_nofollow{64}; | |
197 | // Do not store full paths in std::filesystem::recursive_directory_iterator. | |
198 | // When fs::remove_all uses recursive_directory_iterator::__erase and unlinkat | |
199 | // is available in libc, we do not need the parent directory's path, only the | |
200 | // filenames of the directory entries (and a file descriptor for the parent). | |
201 | // This flag avoids allocating memory for full paths that won't be needed. | |
202 | constexpr fs::directory_options __directory_iterator_filename_only{128}; | |
641cb5a6 JW |
203 | } |
204 | ||
205 | fs::directory_iterator:: | |
206 | directory_iterator(const path& p, directory_options options, error_code* ecptr) | |
207 | { | |
c8bd4dc8 | 208 | // Do not report an error for permission denied errors. |
641cb5a6 JW |
209 | const bool skip_permission_denied |
210 | = is_set(options, directory_options::skip_permission_denied); | |
ebf61754 JW |
211 | // Do not allow opening a symlink. |
212 | const bool nofollow = is_set(options, __directory_iterator_nofollow); | |
641cb5a6 JW |
213 | |
214 | error_code ec; | |
ebf61754 | 215 | _Dir dir(p, skip_permission_denied, nofollow, /*filename only*/false, ec); |
641cb5a6 JW |
216 | |
217 | if (dir.dirp) | |
218 | { | |
da29d2a3 | 219 | auto sp = std::__make_shared<fs::_Dir>(std::move(dir)); |
641cb5a6 JW |
220 | if (sp->advance(skip_permission_denied, ec)) |
221 | _M_dir.swap(sp); | |
222 | } | |
223 | if (ecptr) | |
224 | *ecptr = ec; | |
225 | else if (ec) | |
226 | _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error( | |
227 | "directory iterator cannot open directory", p, ec)); | |
228 | } | |
229 | ||
230 | const fs::directory_entry& | |
044b0434 | 231 | fs::directory_iterator::operator*() const noexcept |
641cb5a6 | 232 | { |
641cb5a6 JW |
233 | return _M_dir->entry; |
234 | } | |
235 | ||
236 | fs::directory_iterator& | |
237 | fs::directory_iterator::operator++() | |
238 | { | |
239 | if (!_M_dir) | |
240 | _GLIBCXX_THROW_OR_ABORT(filesystem_error( | |
241 | "cannot advance non-dereferenceable directory iterator", | |
242 | std::make_error_code(errc::invalid_argument))); | |
243 | if (!_M_dir->advance()) | |
244 | _M_dir.reset(); | |
245 | return *this; | |
246 | } | |
247 | ||
248 | fs::directory_iterator& | |
29453a9f | 249 | fs::directory_iterator::increment(error_code& ec) |
641cb5a6 JW |
250 | { |
251 | if (!_M_dir) | |
252 | { | |
253 | ec = std::make_error_code(errc::invalid_argument); | |
254 | return *this; | |
255 | } | |
256 | if (!_M_dir->advance(ec)) | |
257 | _M_dir.reset(); | |
258 | return *this; | |
259 | } | |
260 | ||
261 | struct fs::recursive_directory_iterator::_Dir_stack : std::stack<_Dir> | |
262 | { | |
ebf61754 | 263 | _Dir_stack(directory_options opts, _Dir&& dir) |
c7dde4a9 JW |
264 | : options(opts), pending(true) |
265 | { | |
ebf61754 | 266 | this->push(std::move(dir)); |
c7dde4a9 JW |
267 | } |
268 | ||
ebf61754 | 269 | path::string_type orig; |
c7dde4a9 JW |
270 | const directory_options options; |
271 | bool pending; | |
272 | ||
641cb5a6 | 273 | void clear() { c.clear(); } |
ebf61754 JW |
274 | |
275 | path current_path() const | |
276 | { | |
277 | path p; | |
278 | if (top().path.empty()) | |
279 | { | |
280 | // Reconstruct path that failed from dir stack. | |
281 | p = orig; | |
282 | for (auto& d : this->c) | |
283 | p /= d.entry.path(); | |
284 | } | |
285 | else | |
286 | p = top().entry.path(); | |
287 | return p; | |
288 | } | |
641cb5a6 JW |
289 | }; |
290 | ||
291 | fs::recursive_directory_iterator:: | |
292 | recursive_directory_iterator(const path& p, directory_options options, | |
293 | error_code* ecptr) | |
641cb5a6 | 294 | { |
ebf61754 JW |
295 | // Do not report an error for permission denied errors. |
296 | const bool skip_permission_denied | |
297 | = is_set(options, directory_options::skip_permission_denied); | |
298 | // Do not allow opening a symlink as the starting directory. | |
299 | const bool nofollow = is_set(options, __directory_iterator_nofollow); | |
300 | // Prefer to store only filenames (not full paths) in directory_entry values. | |
301 | const bool filename_only | |
302 | = is_set(options, __directory_iterator_filename_only); | |
303 | ||
304 | error_code ec; | |
305 | _Dir dir(p, skip_permission_denied, nofollow, filename_only, ec); | |
306 | ||
307 | if (dir.dirp) | |
641cb5a6 | 308 | { |
ebf61754 JW |
309 | auto sp = std::__make_shared<_Dir_stack>(options, std::move(dir)); |
310 | if (ecptr ? sp->top().advance(skip_permission_denied, *ecptr) | |
311 | : sp->top().advance(skip_permission_denied)) | |
641cb5a6 | 312 | { |
ebf61754 JW |
313 | _M_dirs.swap(sp); |
314 | if (filename_only) // Need to save original path for error reporting. | |
315 | _M_dirs->orig = p.native(); | |
641cb5a6 | 316 | } |
641cb5a6 | 317 | } |
ebf61754 JW |
318 | else if (ecptr) |
319 | *ecptr = ec; | |
320 | else if (ec) | |
321 | _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error( | |
322 | "recursive directory iterator cannot open directory", p, ec)); | |
641cb5a6 JW |
323 | } |
324 | ||
325 | fs::recursive_directory_iterator::~recursive_directory_iterator() = default; | |
326 | ||
c7dde4a9 JW |
327 | fs::directory_options |
328 | fs::recursive_directory_iterator::options() const noexcept | |
329 | { | |
330 | return _M_dirs->options; | |
331 | } | |
332 | ||
641cb5a6 | 333 | int |
c7dde4a9 | 334 | fs::recursive_directory_iterator::depth() const noexcept |
641cb5a6 JW |
335 | { |
336 | return int(_M_dirs->size()) - 1; | |
337 | } | |
338 | ||
c7dde4a9 JW |
339 | bool |
340 | fs::recursive_directory_iterator::recursion_pending() const noexcept | |
341 | { | |
342 | return _M_dirs->pending; | |
343 | } | |
344 | ||
641cb5a6 | 345 | const fs::directory_entry& |
c7dde4a9 | 346 | fs::recursive_directory_iterator::operator*() const noexcept |
641cb5a6 JW |
347 | { |
348 | return _M_dirs->top().entry; | |
349 | } | |
350 | ||
351 | fs::recursive_directory_iterator& | |
352 | fs::recursive_directory_iterator:: | |
353 | operator=(const recursive_directory_iterator& other) noexcept = default; | |
354 | ||
355 | fs::recursive_directory_iterator& | |
356 | fs::recursive_directory_iterator:: | |
357 | operator=(recursive_directory_iterator&& other) noexcept = default; | |
358 | ||
359 | fs::recursive_directory_iterator& | |
360 | fs::recursive_directory_iterator::operator++() | |
361 | { | |
362 | error_code ec; | |
363 | increment(ec); | |
364 | if (ec) | |
365 | _GLIBCXX_THROW_OR_ABORT(filesystem_error( | |
366 | "cannot increment recursive directory iterator", ec)); | |
367 | return *this; | |
368 | } | |
369 | ||
370 | fs::recursive_directory_iterator& | |
29453a9f | 371 | fs::recursive_directory_iterator::increment(error_code& ec) |
641cb5a6 JW |
372 | { |
373 | if (!_M_dirs) | |
374 | { | |
375 | ec = std::make_error_code(errc::invalid_argument); | |
376 | return *this; | |
377 | } | |
378 | ||
379 | const bool follow | |
c7dde4a9 | 380 | = is_set(_M_dirs->options, directory_options::follow_directory_symlink); |
641cb5a6 | 381 | const bool skip_permission_denied |
c7dde4a9 | 382 | = is_set(_M_dirs->options, directory_options::skip_permission_denied); |
641cb5a6 JW |
383 | |
384 | auto& top = _M_dirs->top(); | |
385 | ||
c7dde4a9 | 386 | if (std::exchange(_M_dirs->pending, true) && top.should_recurse(follow, ec)) |
641cb5a6 | 387 | { |
ebf61754 | 388 | _Dir dir = top.open_subdir(skip_permission_denied, !follow, ec); |
641cb5a6 JW |
389 | if (ec) |
390 | { | |
391 | _M_dirs.reset(); | |
392 | return *this; | |
393 | } | |
394 | if (dir.dirp) | |
ebf61754 | 395 | _M_dirs->push(std::move(dir)); |
641cb5a6 JW |
396 | } |
397 | ||
398 | while (!_M_dirs->top().advance(skip_permission_denied, ec) && !ec) | |
399 | { | |
400 | _M_dirs->pop(); | |
401 | if (_M_dirs->empty()) | |
402 | { | |
403 | _M_dirs.reset(); | |
404 | return *this; | |
405 | } | |
406 | } | |
ec09a533 JW |
407 | |
408 | if (ec) | |
409 | _M_dirs.reset(); | |
410 | ||
641cb5a6 JW |
411 | return *this; |
412 | } | |
413 | ||
414 | void | |
415 | fs::recursive_directory_iterator::pop(error_code& ec) | |
416 | { | |
417 | if (!_M_dirs) | |
418 | { | |
419 | ec = std::make_error_code(errc::invalid_argument); | |
420 | return; | |
421 | } | |
422 | ||
423 | const bool skip_permission_denied | |
c7dde4a9 | 424 | = is_set(_M_dirs->options, directory_options::skip_permission_denied); |
641cb5a6 JW |
425 | |
426 | do { | |
427 | _M_dirs->pop(); | |
428 | if (_M_dirs->empty()) | |
429 | { | |
430 | _M_dirs.reset(); | |
431 | ec.clear(); | |
432 | return; | |
433 | } | |
ec09a533 JW |
434 | } while (!_M_dirs->top().advance(skip_permission_denied, ec) && !ec); |
435 | ||
436 | if (ec) | |
437 | _M_dirs.reset(); | |
641cb5a6 JW |
438 | } |
439 | ||
440 | void | |
441 | fs::recursive_directory_iterator::pop() | |
442 | { | |
2905e1af | 443 | [[maybe_unused]] const bool dereferenceable = _M_dirs != nullptr; |
641cb5a6 JW |
444 | error_code ec; |
445 | pop(ec); | |
446 | if (ec) | |
ec09a533 | 447 | _GLIBCXX_THROW_OR_ABORT(filesystem_error(dereferenceable |
641cb5a6 JW |
448 | ? "recursive directory iterator cannot pop" |
449 | : "non-dereferenceable recursive directory iterator cannot pop", | |
450 | ec)); | |
451 | } | |
c7dde4a9 JW |
452 | |
453 | void | |
454 | fs::recursive_directory_iterator::disable_recursion_pending() noexcept | |
455 | { | |
456 | _M_dirs->pending = false; | |
457 | } | |
ebf61754 JW |
458 | |
459 | // Used to implement filesystem::remove_all. | |
460 | fs::recursive_directory_iterator& | |
461 | fs::recursive_directory_iterator::__erase(error_code* ecptr) | |
462 | { | |
463 | error_code ec; | |
464 | if (!_M_dirs) | |
465 | { | |
466 | ec = std::make_error_code(errc::invalid_argument); | |
467 | return *this; | |
468 | } | |
469 | ||
470 | // We never want to skip permission denied when removing files. | |
471 | const bool skip_permission_denied = false; | |
472 | // We never want to follow directory symlinks when removing files. | |
473 | const bool nofollow = true; | |
474 | ||
475 | // Loop until we find something we can remove. | |
476 | while (!ec) | |
477 | { | |
478 | auto& top = _M_dirs->top(); | |
479 | ||
5750952b JW |
480 | #if _GLIBCXX_FILESYSTEM_IS_WINDOWS |
481 | // _Dir::unlink uses fs::remove which uses std::system_category() for | |
482 | // Windows errror codes, so we can't just check for EPERM and EISDIR. | |
483 | // Use directory_entry::refresh() here to check if we have a directory. | |
484 | // This can be a TOCTTOU race, but we don't have openat or unlinkat to | |
485 | // solve that on Windows, and generally don't support symlinks anyway. | |
486 | if (top.entry._M_type == file_type::none) | |
487 | top.entry.refresh(); | |
488 | #endif | |
489 | ||
ebf61754 JW |
490 | if (top.entry._M_type == file_type::directory) |
491 | { | |
492 | _Dir dir = top.open_subdir(skip_permission_denied, nofollow, ec); | |
493 | if (!ec) | |
494 | { | |
495 | __glibcxx_assert(dir.dirp != nullptr); | |
496 | if (dir.advance(skip_permission_denied, ec)) | |
497 | { | |
498 | // Non-empty directory, recurse into it. | |
499 | _M_dirs->push(std::move(dir)); | |
500 | continue; | |
501 | } | |
502 | if (!ec) | |
503 | { | |
504 | // Directory is empty so we can remove it. | |
505 | if (top.rmdir(ec)) | |
506 | break; // Success | |
507 | } | |
508 | } | |
509 | } | |
510 | else if (top.unlink(ec)) | |
511 | break; // Success | |
5750952b | 512 | #if ! _GLIBCXX_FILESYSTEM_IS_WINDOWS |
ebf61754 JW |
513 | else if (top.entry._M_type == file_type::none) |
514 | { | |
515 | // We did not have a cached type, so it's possible that top.entry | |
516 | // is actually a directory, and that's why the unlink above failed. | |
517 | #ifdef EPERM | |
5750952b | 518 | // POSIX.1-2017 says unlink on a directory returns EPERM, |
ebf61754 JW |
519 | // but LSB allows EISDIR too. Some targets don't even define EPERM. |
520 | if (ec.value() == EPERM || ec.value() == EISDIR) | |
521 | #else | |
522 | if (ec.value() == EISDIR) | |
523 | #endif | |
524 | { | |
525 | // Retry, treating it as a directory. | |
526 | top.entry._M_type = file_type::directory; | |
527 | ec.clear(); | |
528 | continue; | |
529 | } | |
530 | } | |
5750952b | 531 | #endif |
ebf61754 JW |
532 | } |
533 | ||
534 | if (!ec) | |
535 | { | |
536 | // We successfully removed the current entry, so advance to the next one. | |
537 | if (_M_dirs->top().advance(skip_permission_denied, ec)) | |
538 | return *this; | |
539 | else if (!ec) | |
540 | { | |
541 | // Reached the end of the current directory. | |
542 | _M_dirs->pop(); | |
543 | if (_M_dirs->empty()) | |
544 | _M_dirs.reset(); | |
545 | return *this; | |
546 | } | |
547 | } | |
548 | ||
549 | // Reset _M_dirs to empty. | |
550 | auto dirs = std::move(_M_dirs); | |
551 | ||
552 | // Need to report an error | |
553 | if (ecptr) | |
554 | *ecptr = ec; | |
555 | else | |
556 | _GLIBCXX_THROW_OR_ABORT(fs::filesystem_error("cannot remove all", | |
557 | dirs->orig, | |
558 | dirs->current_path(), | |
559 | ec)); | |
560 | ||
561 | return *this; | |
562 | } |