1 /* 2 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 #include "FilePath.h" 27 28 #include <algorithm> 29 #include <list> 30 31 #ifdef WINDOWS 32 #include <ShellAPI.h> 33 #endif // WINDOWS 34 35 #ifdef POSIX 36 #include <sys/stat.h> 37 #endif // POSIX 38 39 40 bool FilePath::FileExists(const TString FileName) { 41 bool result = false; 42 #ifdef WINDOWS 43 WIN32_FIND_DATA FindFileData; 44 TString fileName = FixPathForPlatform(FileName); 45 HANDLE handle = FindFirstFile(fileName.data(), &FindFileData); 46 47 if (handle != INVALID_HANDLE_VALUE) { 48 if (FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes) { 49 result = true; 50 } 51 else { 52 result = true; 53 } 54 55 FindClose(handle); 56 } 57 #endif // WINDOWS 58 #ifdef POSIX 59 struct stat buf; 60 61 if ((stat(StringToFileSystemString(FileName), &buf) == 0) && 62 (S_ISREG(buf.st_mode) != 0)) { 63 result = true; 64 } 65 #endif // POSIX 66 return result; 67 } 68 69 bool FilePath::DirectoryExists(const TString DirectoryName) { 70 bool result = false; 71 #ifdef WINDOWS 72 WIN32_FIND_DATA FindFileData; 73 TString directoryName = FixPathForPlatform(DirectoryName); 74 HANDLE handle = FindFirstFile(directoryName.data(), &FindFileData); 75 76 if (handle != INVALID_HANDLE_VALUE) { 77 if (FILE_ATTRIBUTE_DIRECTORY & FindFileData.dwFileAttributes) { 78 result = true; 79 } 80 81 FindClose(handle); 82 } 83 #endif // WINDOWS 84 #ifdef POSIX 85 struct stat buf; 86 87 if ((stat(StringToFileSystemString(DirectoryName), &buf) == 0) && 88 (S_ISDIR(buf.st_mode) != 0)) { 89 result = true; 90 } 91 #endif // POSIX 92 return result; 93 } 94 95 #ifdef WINDOWS 96 std::string GetLastErrorAsString() { 97 // Get the error message, if any. 98 DWORD errorMessageID = ::GetLastError(); 99 100 if (errorMessageID == 0) { 101 return "No error message has been recorded"; 102 } 103 104 LPSTR messageBuffer = NULL; 105 size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER 106 | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 107 NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, 108 SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL); 109 110 std::string message(messageBuffer, size); 111 112 // Free the buffer. 113 LocalFree(messageBuffer); 114 115 return message; 116 } 117 #endif // WINDOWS 118 119 bool FilePath::DeleteFile(const TString FileName) { 120 bool result = false; 121 122 if (FileExists(FileName) == true) { 123 #ifdef WINDOWS 124 TString lFileName = FixPathForPlatform(FileName); 125 FileAttributes attributes(lFileName); 126 127 if (attributes.Contains(faReadOnly) == true) { 128 attributes.Remove(faReadOnly); 129 } 130 131 result = ::DeleteFile(lFileName.data()) == TRUE; 132 #endif // WINDOWS 133 #ifdef POSIX 134 if (unlink(StringToFileSystemString(FileName)) == 0) { 135 result = true; 136 } 137 #endif // POSIX 138 } 139 140 return result; 141 } 142 143 bool FilePath::DeleteDirectory(const TString DirectoryName) { 144 bool result = false; 145 146 if (DirectoryExists(DirectoryName) == true) { 147 #ifdef WINDOWS 148 SHFILEOPSTRUCTW fos = {0}; 149 TString directoryName = FixPathForPlatform(DirectoryName); 150 DynamicBuffer<TCHAR> lDirectoryName(directoryName.size() + 2); 151 if (lDirectoryName.GetData() == NULL) { 152 return false; 153 } 154 memcpy(lDirectoryName.GetData(), directoryName.data(), (directoryName.size() + 2) * sizeof(TCHAR)); 155 lDirectoryName[directoryName.size() + 1] = NULL; 156 // Double null terminate for SHFileOperation. 157 158 // Delete the folder and everything inside. 159 fos.wFunc = FO_DELETE; 160 fos.pFrom = lDirectoryName.GetData(); 161 fos.fFlags = FOF_NO_UI; 162 result = SHFileOperation(&fos) == 0; 163 #endif // WINDOWS 164 #ifdef POSIX 165 if (unlink(StringToFileSystemString(DirectoryName)) == 0) { 166 result = true; 167 } 168 #endif // POSIX 169 } 170 171 return result; 172 } 173 174 TString FilePath::IncludeTrailingSeparator(const TString value) { 175 TString result = value; 176 177 if (value.size() > 0) { 178 TString::iterator i = result.end(); 179 i--; 180 181 if (*i != TRAILING_PATHSEPARATOR) { 182 result += TRAILING_PATHSEPARATOR; 183 } 184 } 185 186 return result; 187 } 188 189 TString FilePath::IncludeTrailingSeparator(const char* value) { 190 TString lvalue = PlatformString(value).toString(); 191 return IncludeTrailingSeparator(lvalue); 192 } 193 194 TString FilePath::IncludeTrailingSeparator(const wchar_t* value) { 195 TString lvalue = PlatformString(value).toString(); 196 return IncludeTrailingSeparator(lvalue); 197 } 198 199 TString FilePath::ExtractFilePath(TString Path) { 200 #ifdef WINDOWS 201 TString result; 202 size_t slash = Path.find_last_of(TRAILING_PATHSEPARATOR); 203 if (slash != TString::npos) 204 result = Path.substr(0, slash); 205 return result; 206 #endif // WINDOWS 207 #ifdef POSIX 208 return dirname(StringToFileSystemString(Path)); 209 #endif // POSIX 210 } 211 212 TString FilePath::ExtractFileExt(TString Path) { 213 TString result; 214 size_t dot = Path.find_last_of('.'); 215 216 if (dot != TString::npos) { 217 result = Path.substr(dot, Path.size() - dot); 218 } 219 220 return result; 221 } 222 223 TString FilePath::ExtractFileName(TString Path) { 224 #ifdef WINDOWS 225 TString result; 226 227 size_t slash = Path.find_last_of(TRAILING_PATHSEPARATOR); 228 if (slash != TString::npos) 229 result = Path.substr(slash + 1, Path.size() - slash - 1); 230 231 return result; 232 #endif // WINDOWS 233 #ifdef POSIX 234 return basename(StringToFileSystemString(Path)); 235 #endif // POSIX 236 } 237 238 TString FilePath::ChangeFileExt(TString Path, TString Extension) { 239 TString result; 240 size_t dot = Path.find_last_of('.'); 241 242 if (dot != TString::npos) { 243 result = Path.substr(0, dot) + Extension; 244 } 245 246 if (result.empty() == true) { 247 result = Path; 248 } 249 250 return result; 251 } 252 253 TString FilePath::FixPathForPlatform(TString Path) { 254 TString result = Path; 255 std::replace(result.begin(), result.end(), 256 BAD_TRAILING_PATHSEPARATOR, TRAILING_PATHSEPARATOR); 257 #ifdef WINDOWS 258 // The maximum path that does not require long path prefix. On Windows the 259 // maximum path is 260 minus 1 (NUL) but for directories it is 260 minus 260 // 12 minus 1 (to allow for the creation of a 8.3 file in the directory). 261 const int maxPath = 247; 262 if (result.length() > maxPath && 263 result.find(_T("\\\\?\\")) == TString::npos && 264 result.find(_T("\\\\?\\UNC")) == TString::npos) { 265 const TString prefix(_T("\\\\")); 266 if (!result.compare(0, prefix.size(), prefix)) { 267 // UNC path, converting to UNC path in long notation 268 result = _T("\\\\?\\UNC") + result.substr(1, result.length()); 269 } else { 270 // converting to non-UNC path in long notation 271 result = _T("\\\\?\\") + result; 272 } 273 } 274 #endif // WINDOWS 275 return result; 276 } 277 278 TString FilePath::FixPathSeparatorForPlatform(TString Path) { 279 TString result = Path; 280 std::replace(result.begin(), result.end(), 281 BAD_PATH_SEPARATOR, PATH_SEPARATOR); 282 return result; 283 } 284 285 TString FilePath::PathSeparator() { 286 TString result; 287 result = PATH_SEPARATOR; 288 return result; 289 } 290 291 bool FilePath::CreateDirectory(TString Path, bool ownerOnly) { 292 bool result = false; 293 294 std::list<TString> paths; 295 TString lpath = Path; 296 297 while (lpath.empty() == false && DirectoryExists(lpath) == false) { 298 paths.push_front(lpath); 299 lpath = ExtractFilePath(lpath); 300 } 301 302 for (std::list<TString>::iterator iterator = paths.begin(); 303 iterator != paths.end(); iterator++) { 304 lpath = *iterator; 305 306 #ifdef WINDOWS 307 if (_wmkdir(lpath.data()) == 0) { 308 #endif // WINDOWS 309 #ifdef POSIX 310 mode_t mode = S_IRWXU; 311 if (!ownerOnly) { 312 mode |= S_IRWXG | S_IROTH | S_IXOTH; 313 } 314 if (mkdir(StringToFileSystemString(lpath), mode) == 0) { 315 #endif // POSIX 316 result = true; 317 } 318 else { 319 result = false; 320 break; 321 } 322 } 323 324 return result; 325 } 326 327 void FilePath::ChangePermissions(TString FileName, bool ownerOnly) { 328 #ifdef POSIX 329 mode_t mode = S_IRWXU; 330 if (!ownerOnly) { 331 mode |= S_IRWXG | S_IROTH | S_IXOTH; 332 } 333 chmod(FileName.data(), mode); 334 #endif // POSIX 335 } 336 337 //---------------------------------------------------------------------------- 338 339 #include <algorithm> 340 341 FileAttributes::FileAttributes(const TString FileName, bool FollowLink) { 342 FFileName = FileName; 343 FFollowLink = FollowLink; 344 ReadAttributes(); 345 } 346 347 bool FileAttributes::WriteAttributes() { 348 bool result = false; 349 350 #ifdef WINDOWS 351 DWORD attributes = 0; 352 353 for (std::vector<FileAttribute>::const_iterator iterator = 354 FAttributes.begin(); 355 iterator != FAttributes.end(); iterator++) { 356 switch (*iterator) { 357 case faArchive: { 358 attributes = attributes & FILE_ATTRIBUTE_ARCHIVE; 359 break; 360 } 361 case faCompressed: { 362 attributes = attributes & FILE_ATTRIBUTE_COMPRESSED; 363 break; 364 } 365 case faDevice: { 366 attributes = attributes & FILE_ATTRIBUTE_DEVICE; 367 break; 368 } 369 case faDirectory: { 370 attributes = attributes & FILE_ATTRIBUTE_DIRECTORY; 371 break; 372 } 373 case faEncrypted: { 374 attributes = attributes & FILE_ATTRIBUTE_ENCRYPTED; 375 break; 376 } 377 case faHidden: { 378 attributes = attributes & FILE_ATTRIBUTE_HIDDEN; 379 break; 380 } 381 case faNormal: { 382 attributes = attributes & FILE_ATTRIBUTE_NORMAL; 383 break; 384 } 385 case faNotContentIndexed: { 386 attributes = attributes & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; 387 break; 388 } 389 case faOffline: { 390 attributes = attributes & FILE_ATTRIBUTE_OFFLINE; 391 break; 392 } 393 case faSystem: { 394 attributes = attributes & FILE_ATTRIBUTE_SYSTEM; 395 break; 396 } 397 case faSymbolicLink: { 398 attributes = attributes & FILE_ATTRIBUTE_REPARSE_POINT; 399 break; 400 } 401 case faSparceFile: { 402 attributes = attributes & FILE_ATTRIBUTE_SPARSE_FILE; 403 break; 404 } 405 case faReadOnly: { 406 attributes = attributes & FILE_ATTRIBUTE_READONLY; 407 break; 408 } 409 case faTemporary: { 410 attributes = attributes & FILE_ATTRIBUTE_TEMPORARY; 411 break; 412 } 413 case faVirtual: { 414 attributes = attributes & FILE_ATTRIBUTE_VIRTUAL; 415 break; 416 } 417 } 418 } 419 420 if (::SetFileAttributes(FFileName.data(), attributes) != 0) { 421 result = true; 422 } 423 #endif // WINDOWS 424 #ifdef POSIX 425 mode_t attributes = 0; 426 427 for (std::vector<FileAttribute>::const_iterator iterator = 428 FAttributes.begin(); 429 iterator != FAttributes.end(); iterator++) { 430 switch (*iterator) { 431 case faBlockSpecial: { 432 attributes |= S_IFBLK; 433 break; 434 } 435 case faCharacterSpecial: { 436 attributes |= S_IFCHR; 437 break; 438 } 439 case faFIFOSpecial: { 440 attributes |= S_IFIFO; 441 break; 442 } 443 case faNormal: { 444 attributes |= S_IFREG; 445 break; 446 } 447 case faDirectory: { 448 attributes |= S_IFDIR; 449 break; 450 } 451 case faSymbolicLink: { 452 attributes |= S_IFLNK; 453 break; 454 } 455 case faSocket: { 456 attributes |= S_IFSOCK; 457 break; 458 } 459 460 // Owner 461 case faReadOnly: { 462 attributes |= S_IRUSR; 463 break; 464 } 465 case faWriteOnly: { 466 attributes |= S_IWUSR; 467 break; 468 } 469 case faReadWrite: { 470 attributes |= S_IRUSR; 471 attributes |= S_IWUSR; 472 break; 473 } 474 case faExecute: { 475 attributes |= S_IXUSR; 476 break; 477 } 478 479 // Group 480 case faGroupReadOnly: { 481 attributes |= S_IRGRP; 482 break; 483 } 484 case faGroupWriteOnly: { 485 attributes |= S_IWGRP; 486 break; 487 } 488 case faGroupReadWrite: { 489 attributes |= S_IRGRP; 490 attributes |= S_IWGRP; 491 break; 492 } 493 case faGroupExecute: { 494 attributes |= S_IXGRP; 495 break; 496 } 497 498 // Others 499 case faOthersReadOnly: { 500 attributes |= S_IROTH; 501 break; 502 } 503 case faOthersWriteOnly: { 504 attributes |= S_IWOTH; 505 break; 506 } 507 case faOthersReadWrite: { 508 attributes |= S_IROTH; 509 attributes |= S_IWOTH; 510 break; 511 } 512 case faOthersExecute: { 513 attributes |= S_IXOTH; 514 break; 515 } 516 default: 517 break; 518 } 519 } 520 521 if (chmod(FFileName.data(), attributes) == 0) { 522 result = true; 523 } 524 #endif // POSIX 525 526 return result; 527 } 528 529 #define S_ISRUSR(m) (((m) & S_IRWXU) == S_IRUSR) 530 #define S_ISWUSR(m) (((m) & S_IRWXU) == S_IWUSR) 531 #define S_ISXUSR(m) (((m) & S_IRWXU) == S_IXUSR) 532 533 #define S_ISRGRP(m) (((m) & S_IRWXG) == S_IRGRP) 534 #define S_ISWGRP(m) (((m) & S_IRWXG) == S_IWGRP) 535 #define S_ISXGRP(m) (((m) & S_IRWXG) == S_IXGRP) 536 537 #define S_ISROTH(m) (((m) & S_IRWXO) == S_IROTH) 538 #define S_ISWOTH(m) (((m) & S_IRWXO) == S_IWOTH) 539 #define S_ISXOTH(m) (((m) & S_IRWXO) == S_IXOTH) 540 541 bool FileAttributes::ReadAttributes() { 542 bool result = false; 543 544 #ifdef WINDOWS 545 DWORD attributes = ::GetFileAttributes(FFileName.data()); 546 547 if (attributes != INVALID_FILE_ATTRIBUTES) { 548 result = true; 549 550 if (attributes | FILE_ATTRIBUTE_ARCHIVE) { 551 FAttributes.push_back(faArchive); 552 } 553 if (attributes | FILE_ATTRIBUTE_COMPRESSED) { 554 FAttributes.push_back(faCompressed); 555 } 556 if (attributes | FILE_ATTRIBUTE_DEVICE) { 557 FAttributes.push_back(faDevice); 558 } 559 if (attributes | FILE_ATTRIBUTE_DIRECTORY) { 560 FAttributes.push_back(faDirectory); 561 } 562 if (attributes | FILE_ATTRIBUTE_ENCRYPTED) { 563 FAttributes.push_back(faEncrypted); 564 } 565 if (attributes | FILE_ATTRIBUTE_HIDDEN) { 566 FAttributes.push_back(faHidden); 567 } 568 // if (attributes | FILE_ATTRIBUTE_INTEGRITY_STREAM) { 569 // FAttributes.push_back(faIntegrityStream); 570 // } 571 if (attributes | FILE_ATTRIBUTE_NORMAL) { 572 FAttributes.push_back(faNormal); 573 } 574 if (attributes | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) { 575 FAttributes.push_back(faNotContentIndexed); 576 } 577 // if (attributes | FILE_ATTRIBUTE_NO_SCRUB_DATA) { 578 // FAttributes.push_back(faNoScrubData); 579 // } 580 if (attributes | FILE_ATTRIBUTE_SYSTEM) { 581 FAttributes.push_back(faSystem); 582 } 583 if (attributes | FILE_ATTRIBUTE_OFFLINE) { 584 FAttributes.push_back(faOffline); 585 } 586 if (attributes | FILE_ATTRIBUTE_REPARSE_POINT) { 587 FAttributes.push_back(faSymbolicLink); 588 } 589 if (attributes | FILE_ATTRIBUTE_SPARSE_FILE) { 590 FAttributes.push_back(faSparceFile); 591 } 592 if (attributes | FILE_ATTRIBUTE_READONLY ) { 593 FAttributes.push_back(faReadOnly); 594 } 595 if (attributes | FILE_ATTRIBUTE_TEMPORARY) { 596 FAttributes.push_back(faTemporary); 597 } 598 if (attributes | FILE_ATTRIBUTE_VIRTUAL) { 599 FAttributes.push_back(faVirtual); 600 } 601 } 602 #endif // WINDOWS 603 #ifdef POSIX 604 struct stat status; 605 606 if (stat(StringToFileSystemString(FFileName), &status) == 0) { 607 result = true; 608 609 if (S_ISBLK(status.st_mode) != 0) { 610 FAttributes.push_back(faBlockSpecial); 611 } 612 if (S_ISCHR(status.st_mode) != 0) { 613 FAttributes.push_back(faCharacterSpecial); 614 } 615 if (S_ISFIFO(status.st_mode) != 0) { 616 FAttributes.push_back(faFIFOSpecial); 617 } 618 if (S_ISREG(status.st_mode) != 0) { 619 FAttributes.push_back(faNormal); 620 } 621 if (S_ISDIR(status.st_mode) != 0) { 622 FAttributes.push_back(faDirectory); 623 } 624 if (S_ISLNK(status.st_mode) != 0) { 625 FAttributes.push_back(faSymbolicLink); 626 } 627 if (S_ISSOCK(status.st_mode) != 0) { 628 FAttributes.push_back(faSocket); 629 } 630 631 // Owner 632 if (S_ISRUSR(status.st_mode) != 0) { 633 if (S_ISWUSR(status.st_mode) != 0) { 634 FAttributes.push_back(faReadWrite); 635 } else { 636 FAttributes.push_back(faReadOnly); 637 } 638 } else if (S_ISWUSR(status.st_mode) != 0) { 639 FAttributes.push_back(faWriteOnly); 640 } 641 642 if (S_ISXUSR(status.st_mode) != 0) { 643 FAttributes.push_back(faExecute); 644 } 645 646 // Group 647 if (S_ISRGRP(status.st_mode) != 0) { 648 if (S_ISWGRP(status.st_mode) != 0) { 649 FAttributes.push_back(faGroupReadWrite); 650 } else { 651 FAttributes.push_back(faGroupReadOnly); 652 } 653 } else if (S_ISWGRP(status.st_mode) != 0) { 654 FAttributes.push_back(faGroupWriteOnly); 655 } 656 657 if (S_ISXGRP(status.st_mode) != 0) { 658 FAttributes.push_back(faGroupExecute); 659 } 660 661 662 // Others 663 if (S_ISROTH(status.st_mode) != 0) { 664 if (S_ISWOTH(status.st_mode) != 0) { 665 FAttributes.push_back(faOthersReadWrite); 666 } else { 667 FAttributes.push_back(faOthersReadOnly); 668 } 669 } 670 else if (S_ISWOTH(status.st_mode) != 0) { 671 FAttributes.push_back(faOthersWriteOnly); 672 } 673 674 if (S_ISXOTH(status.st_mode) != 0) { 675 FAttributes.push_back(faOthersExecute); 676 } 677 678 if (FFileName.size() > 0 && FFileName[0] == '.') { 679 FAttributes.push_back(faHidden); 680 } 681 } 682 #endif // POSIX 683 684 return result; 685 } 686 687 bool FileAttributes::Valid(const FileAttribute Value) { 688 bool result = false; 689 690 switch (Value) { 691 #ifdef WINDOWS 692 case faHidden: 693 #endif // WINDOWS 694 #ifdef POSIX 695 case faReadWrite: 696 case faWriteOnly: 697 case faExecute: 698 699 case faGroupReadWrite: 700 case faGroupWriteOnly: 701 case faGroupReadOnly: 702 case faGroupExecute: 703 704 case faOthersReadWrite: 705 case faOthersWriteOnly: 706 case faOthersReadOnly: 707 case faOthersExecute: 708 #endif // POSIX 709 710 case faReadOnly: { 711 result = true; 712 break; 713 } 714 default: 715 break; 716 } 717 718 return result; 719 } 720 721 void FileAttributes::Append(FileAttribute Value) { 722 if (Valid(Value) == true) { 723 #ifdef POSIX 724 if ((Value == faReadOnly && Contains(faWriteOnly) == true) || 725 (Value == faWriteOnly && Contains(faReadOnly) == true)) { 726 Value = faReadWrite; 727 } 728 #endif // POSIX 729 730 FAttributes.push_back(Value); 731 WriteAttributes(); 732 } 733 } 734 735 bool FileAttributes::Contains(FileAttribute Value) { 736 bool result = false; 737 738 std::vector<FileAttribute>::const_iterator iterator = 739 std::find(FAttributes.begin(), FAttributes.end(), Value); 740 741 if (iterator != FAttributes.end()) { 742 result = true; 743 } 744 745 return result; 746 } 747 748 void FileAttributes::Remove(FileAttribute Value) { 749 if (Valid(Value) == true) { 750 #ifdef POSIX 751 if (Value == faReadOnly && Contains(faReadWrite) == true) { 752 Append(faWriteOnly); 753 Remove(faReadWrite); 754 } 755 else if (Value == faWriteOnly && Contains(faReadWrite) == true) { 756 Append(faReadOnly); 757 Remove(faReadWrite); 758 } 759 #endif // POSIX 760 761 std::vector<FileAttribute>::iterator iterator = 762 std::find(FAttributes.begin(), FAttributes.end(), Value); 763 764 if (iterator != FAttributes.end()) { 765 FAttributes.erase(iterator); 766 WriteAttributes(); 767 } 768 } 769 }