1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "base/nix/mime_util_xdg.h" 6 7 #include <cstdlib> 8 #include <list> 9 #include <map> 10 #include <vector> 11 12 #include "base/environment.h" 13 #include "base/file_util.h" 14 #include "base/lazy_instance.h" 15 #include "base/logging.h" 16 #include "base/memory/scoped_ptr.h" 17 #include "base/memory/singleton.h" 18 #include "base/nix/xdg_util.h" 19 #include "base/strings/string_split.h" 20 #include "base/strings/string_util.h" 21 #include "base/synchronization/lock.h" 22 #include "base/third_party/xdg_mime/xdgmime.h" 23 #include "base/threading/thread_restrictions.h" 24 #include "base/time/time.h" 25 26 namespace base { 27 namespace nix { 28 29 namespace { 30 31 class IconTheme; 32 33 // None of the XDG stuff is thread-safe, so serialize all access under 34 // this lock. 35 base::LazyInstance<base::Lock>::Leaky 36 g_mime_util_xdg_lock = LAZY_INSTANCE_INITIALIZER; 37 38 class MimeUtilConstants { 39 public: 40 typedef std::map<std::string, IconTheme*> IconThemeMap; 41 typedef std::map<FilePath, base::Time> IconDirMtimeMap; 42 typedef std::vector<std::string> IconFormats; 43 44 // Specified by XDG icon theme specs. 45 static const int kUpdateIntervalInSeconds = 5; 46 47 static const size_t kDefaultThemeNum = 4; 48 49 static MimeUtilConstants* GetInstance() { 50 return Singleton<MimeUtilConstants>::get(); 51 } 52 53 // Store icon directories and their mtimes. 54 IconDirMtimeMap icon_dirs_; 55 56 // Store icon formats. 57 IconFormats icon_formats_; 58 59 // Store loaded icon_theme. 60 IconThemeMap icon_themes_; 61 62 // The default theme. 63 IconTheme* default_themes_[kDefaultThemeNum]; 64 65 base::TimeTicks last_check_time_; 66 67 // The current icon theme, usually set through GTK theme integration. 68 std::string icon_theme_name_; 69 70 private: 71 MimeUtilConstants() { 72 icon_formats_.push_back(".png"); 73 icon_formats_.push_back(".svg"); 74 icon_formats_.push_back(".xpm"); 75 76 for (size_t i = 0; i < kDefaultThemeNum; ++i) 77 default_themes_[i] = NULL; 78 } 79 ~MimeUtilConstants(); 80 81 friend struct DefaultSingletonTraits<MimeUtilConstants>; 82 83 DISALLOW_COPY_AND_ASSIGN(MimeUtilConstants); 84 }; 85 86 // IconTheme represents an icon theme as defined by the xdg icon theme spec. 87 // Example themes on GNOME include 'Human' and 'Mist'. 88 // Example themes on KDE include 'crystalsvg' and 'kdeclassic'. 89 class IconTheme { 90 public: 91 // A theme consists of multiple sub-directories, like '32x32' and 'scalable'. 92 class SubDirInfo { 93 public: 94 // See spec for details. 95 enum Type { 96 Fixed, 97 Scalable, 98 Threshold 99 }; 100 SubDirInfo() 101 : size(0), 102 type(Threshold), 103 max_size(0), 104 min_size(0), 105 threshold(2) { 106 } 107 size_t size; // Nominal size of the icons in this directory. 108 Type type; // Type of the icon size. 109 size_t max_size; // Maximum size that the icons can be scaled to. 110 size_t min_size; // Minimum size that the icons can be scaled to. 111 size_t threshold; // Maximum difference from desired size. 2 by default. 112 }; 113 114 explicit IconTheme(const std::string& name); 115 116 ~IconTheme() {} 117 118 // Returns the path to an icon with the name |icon_name| and a size of |size| 119 // pixels. If the icon does not exist, but |inherits| is true, then look for 120 // the icon in the parent theme. 121 FilePath GetIconPath(const std::string& icon_name, int size, bool inherits); 122 123 // Load a theme with the name |theme_name| into memory. Returns null if theme 124 // is invalid. 125 static IconTheme* LoadTheme(const std::string& theme_name); 126 127 private: 128 // Returns the path to an icon with the name |icon_name| in |subdir|. 129 FilePath GetIconPathUnderSubdir(const std::string& icon_name, 130 const std::string& subdir); 131 132 // Whether the theme loaded properly. 133 bool IsValid() { 134 return index_theme_loaded_; 135 } 136 137 // Read and parse |file| which is usually named 'index.theme' per theme spec. 138 bool LoadIndexTheme(const FilePath& file); 139 140 // Checks to see if the icons in |info| matches |size| (in pixels). Returns 141 // 0 if they match, or the size difference in pixels. 142 size_t MatchesSize(SubDirInfo* info, size_t size); 143 144 // Yet another function to read a line. 145 std::string ReadLine(FILE* fp); 146 147 // Set directories to search for icons to the comma-separated list |dirs|. 148 bool SetDirectories(const std::string& dirs); 149 150 bool index_theme_loaded_; // True if an instance is properly loaded. 151 // store the scattered directories of this theme. 152 std::list<FilePath> dirs_; 153 154 // store the subdirs of this theme and array index of |info_array_|. 155 std::map<std::string, int> subdirs_; 156 scoped_ptr<SubDirInfo[]> info_array_; // List of sub-directories. 157 std::string inherits_; // Name of the theme this one inherits from. 158 }; 159 160 IconTheme::IconTheme(const std::string& name) 161 : index_theme_loaded_(false) { 162 base::ThreadRestrictions::AssertIOAllowed(); 163 // Iterate on all icon directories to find directories of the specified 164 // theme and load the first encountered index.theme. 165 MimeUtilConstants::IconDirMtimeMap::iterator iter; 166 FilePath theme_path; 167 MimeUtilConstants::IconDirMtimeMap* icon_dirs = 168 &MimeUtilConstants::GetInstance()->icon_dirs_; 169 for (iter = icon_dirs->begin(); iter != icon_dirs->end(); ++iter) { 170 theme_path = iter->first.Append(name); 171 if (!DirectoryExists(theme_path)) 172 continue; 173 FilePath theme_index = theme_path.Append("index.theme"); 174 if (!index_theme_loaded_ && PathExists(theme_index)) { 175 if (!LoadIndexTheme(theme_index)) 176 return; 177 index_theme_loaded_ = true; 178 } 179 dirs_.push_back(theme_path); 180 } 181 } 182 183 FilePath IconTheme::GetIconPath(const std::string& icon_name, int size, 184 bool inherits) { 185 std::map<std::string, int>::iterator subdir_iter; 186 FilePath icon_path; 187 188 for (subdir_iter = subdirs_.begin(); 189 subdir_iter != subdirs_.end(); 190 ++subdir_iter) { 191 SubDirInfo* info = &info_array_[subdir_iter->second]; 192 if (MatchesSize(info, size) == 0) { 193 icon_path = GetIconPathUnderSubdir(icon_name, subdir_iter->first); 194 if (!icon_path.empty()) 195 return icon_path; 196 } 197 } 198 // Now looking for the mostly matched. 199 size_t min_delta_seen = 9999; 200 201 for (subdir_iter = subdirs_.begin(); 202 subdir_iter != subdirs_.end(); 203 ++subdir_iter) { 204 SubDirInfo* info = &info_array_[subdir_iter->second]; 205 size_t delta = MatchesSize(info, size); 206 if (delta < min_delta_seen) { 207 FilePath path = GetIconPathUnderSubdir(icon_name, subdir_iter->first); 208 if (!path.empty()) { 209 min_delta_seen = delta; 210 icon_path = path; 211 } 212 } 213 } 214 215 if (!icon_path.empty() || !inherits || inherits_ == "") 216 return icon_path; 217 218 IconTheme* theme = LoadTheme(inherits_); 219 // Inheriting from itself means the theme is buggy but we shouldn't crash. 220 if (theme && theme != this) 221 return theme->GetIconPath(icon_name, size, inherits); 222 else 223 return FilePath(); 224 } 225 226 IconTheme* IconTheme::LoadTheme(const std::string& theme_name) { 227 scoped_ptr<IconTheme> theme; 228 MimeUtilConstants::IconThemeMap* icon_themes = 229 &MimeUtilConstants::GetInstance()->icon_themes_; 230 if (icon_themes->find(theme_name) != icon_themes->end()) { 231 theme.reset((*icon_themes)[theme_name]); 232 } else { 233 theme.reset(new IconTheme(theme_name)); 234 if (!theme->IsValid()) 235 theme.reset(); 236 (*icon_themes)[theme_name] = theme.get(); 237 } 238 return theme.release(); 239 } 240 241 FilePath IconTheme::GetIconPathUnderSubdir(const std::string& icon_name, 242 const std::string& subdir) { 243 FilePath icon_path; 244 std::list<FilePath>::iterator dir_iter; 245 MimeUtilConstants::IconFormats* icon_formats = 246 &MimeUtilConstants::GetInstance()->icon_formats_; 247 for (dir_iter = dirs_.begin(); dir_iter != dirs_.end(); ++dir_iter) { 248 for (size_t i = 0; i < icon_formats->size(); ++i) { 249 icon_path = dir_iter->Append(subdir); 250 icon_path = icon_path.Append(icon_name + (*icon_formats)[i]); 251 if (PathExists(icon_path)) 252 return icon_path; 253 } 254 } 255 return FilePath(); 256 } 257 258 bool IconTheme::LoadIndexTheme(const FilePath& file) { 259 FILE* fp = file_util::OpenFile(file, "r"); 260 SubDirInfo* current_info = NULL; 261 if (!fp) 262 return false; 263 264 // Read entries. 265 while (!feof(fp) && !ferror(fp)) { 266 std::string buf = ReadLine(fp); 267 if (buf == "") 268 break; 269 270 std::string entry; 271 TrimWhitespaceASCII(buf, TRIM_ALL, &entry); 272 if (entry.length() == 0 || entry[0] == '#') { 273 // Blank line or Comment. 274 continue; 275 } else if (entry[0] == '[' && info_array_.get()) { 276 current_info = NULL; 277 std::string subdir = entry.substr(1, entry.length() - 2); 278 if (subdirs_.find(subdir) != subdirs_.end()) 279 current_info = &info_array_[subdirs_[subdir]]; 280 } 281 282 std::string key, value; 283 std::vector<std::string> r; 284 base::SplitStringDontTrim(entry, '=', &r); 285 if (r.size() < 2) 286 continue; 287 288 TrimWhitespaceASCII(r[0], TRIM_ALL, &key); 289 for (size_t i = 1; i < r.size(); i++) 290 value.append(r[i]); 291 TrimWhitespaceASCII(value, TRIM_ALL, &value); 292 293 if (current_info) { 294 if (key == "Size") { 295 current_info->size = atoi(value.c_str()); 296 } else if (key == "Type") { 297 if (value == "Fixed") 298 current_info->type = SubDirInfo::Fixed; 299 else if (value == "Scalable") 300 current_info->type = SubDirInfo::Scalable; 301 else if (value == "Threshold") 302 current_info->type = SubDirInfo::Threshold; 303 } else if (key == "MaxSize") { 304 current_info->max_size = atoi(value.c_str()); 305 } else if (key == "MinSize") { 306 current_info->min_size = atoi(value.c_str()); 307 } else if (key == "Threshold") { 308 current_info->threshold = atoi(value.c_str()); 309 } 310 } else { 311 if (key.compare("Directories") == 0 && !info_array_.get()) { 312 if (!SetDirectories(value)) break; 313 } else if (key.compare("Inherits") == 0) { 314 if (value != "hicolor") 315 inherits_ = value; 316 } 317 } 318 } 319 320 file_util::CloseFile(fp); 321 return info_array_.get() != NULL; 322 } 323 324 size_t IconTheme::MatchesSize(SubDirInfo* info, size_t size) { 325 if (info->type == SubDirInfo::Fixed) { 326 if (size > info->size) 327 return size - info->size; 328 else 329 return info->size - size; 330 } else if (info->type == SubDirInfo::Scalable) { 331 if (size < info->min_size) 332 return info->min_size - size; 333 if (size > info->max_size) 334 return size - info->max_size; 335 return 0; 336 } else { 337 if (size + info->threshold < info->size) 338 return info->size - size - info->threshold; 339 if (size > info->size + info->threshold) 340 return size - info->size - info->threshold; 341 return 0; 342 } 343 } 344 345 std::string IconTheme::ReadLine(FILE* fp) { 346 if (!fp) 347 return std::string(); 348 349 std::string result; 350 const size_t kBufferSize = 100; 351 char buffer[kBufferSize]; 352 while ((fgets(buffer, kBufferSize - 1, fp)) != NULL) { 353 result += buffer; 354 size_t len = result.length(); 355 if (len == 0) 356 break; 357 char end = result[len - 1]; 358 if (end == '\n' || end == '\0') 359 break; 360 } 361 362 return result; 363 } 364 365 bool IconTheme::SetDirectories(const std::string& dirs) { 366 int num = 0; 367 std::string::size_type pos = 0, epos; 368 std::string dir; 369 while ((epos = dirs.find(',', pos)) != std::string::npos) { 370 TrimWhitespaceASCII(dirs.substr(pos, epos - pos), TRIM_ALL, &dir); 371 if (dir.length() == 0) { 372 DLOG(WARNING) << "Invalid index.theme: blank subdir"; 373 return false; 374 } 375 subdirs_[dir] = num++; 376 pos = epos + 1; 377 } 378 TrimWhitespaceASCII(dirs.substr(pos), TRIM_ALL, &dir); 379 if (dir.length() == 0) { 380 DLOG(WARNING) << "Invalid index.theme: blank subdir"; 381 return false; 382 } 383 subdirs_[dir] = num++; 384 info_array_.reset(new SubDirInfo[num]); 385 return true; 386 } 387 388 bool CheckDirExistsAndGetMtime(const FilePath& dir, 389 base::Time* last_modified) { 390 if (!DirectoryExists(dir)) 391 return false; 392 base::PlatformFileInfo file_info; 393 if (!file_util::GetFileInfo(dir, &file_info)) 394 return false; 395 *last_modified = file_info.last_modified; 396 return true; 397 } 398 399 // Make sure |dir| exists and add it to the list of icon directories. 400 void TryAddIconDir(const FilePath& dir) { 401 base::Time last_modified; 402 if (!CheckDirExistsAndGetMtime(dir, &last_modified)) 403 return; 404 MimeUtilConstants::GetInstance()->icon_dirs_[dir] = last_modified; 405 } 406 407 // For a xdg directory |dir|, add the appropriate icon sub-directories. 408 void AddXDGDataDir(const FilePath& dir) { 409 if (!DirectoryExists(dir)) 410 return; 411 TryAddIconDir(dir.Append("icons")); 412 TryAddIconDir(dir.Append("pixmaps")); 413 } 414 415 // Add all the xdg icon directories. 416 void InitIconDir() { 417 FilePath home = file_util::GetHomeDir(); 418 if (!home.empty()) { 419 FilePath legacy_data_dir(home); 420 legacy_data_dir = legacy_data_dir.AppendASCII(".icons"); 421 if (DirectoryExists(legacy_data_dir)) 422 TryAddIconDir(legacy_data_dir); 423 } 424 const char* env = getenv("XDG_DATA_HOME"); 425 if (env) { 426 AddXDGDataDir(FilePath(env)); 427 } else if (!home.empty()) { 428 FilePath local_data_dir(home); 429 local_data_dir = local_data_dir.AppendASCII(".local"); 430 local_data_dir = local_data_dir.AppendASCII("share"); 431 AddXDGDataDir(local_data_dir); 432 } 433 434 env = getenv("XDG_DATA_DIRS"); 435 if (!env) { 436 AddXDGDataDir(FilePath("/usr/local/share")); 437 AddXDGDataDir(FilePath("/usr/share")); 438 } else { 439 std::string xdg_data_dirs = env; 440 std::string::size_type pos = 0, epos; 441 while ((epos = xdg_data_dirs.find(':', pos)) != std::string::npos) { 442 AddXDGDataDir(FilePath(xdg_data_dirs.substr(pos, epos - pos))); 443 pos = epos + 1; 444 } 445 AddXDGDataDir(FilePath(xdg_data_dirs.substr(pos))); 446 } 447 } 448 449 void EnsureUpdated() { 450 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 451 if (constants->last_check_time_.is_null()) { 452 constants->last_check_time_ = base::TimeTicks::Now(); 453 InitIconDir(); 454 return; 455 } 456 457 // Per xdg theme spec, we should check the icon directories every so often 458 // for newly added icons. 459 base::TimeDelta time_since_last_check = 460 base::TimeTicks::Now() - constants->last_check_time_; 461 if (time_since_last_check.InSeconds() > constants->kUpdateIntervalInSeconds) { 462 constants->last_check_time_ += time_since_last_check; 463 464 bool rescan_icon_dirs = false; 465 MimeUtilConstants::IconDirMtimeMap* icon_dirs = &constants->icon_dirs_; 466 MimeUtilConstants::IconDirMtimeMap::iterator iter; 467 for (iter = icon_dirs->begin(); iter != icon_dirs->end(); ++iter) { 468 base::Time last_modified; 469 if (!CheckDirExistsAndGetMtime(iter->first, &last_modified) || 470 last_modified != iter->second) { 471 rescan_icon_dirs = true; 472 break; 473 } 474 } 475 476 if (rescan_icon_dirs) { 477 constants->icon_dirs_.clear(); 478 constants->icon_themes_.clear(); 479 InitIconDir(); 480 } 481 } 482 } 483 484 // Find a fallback icon if we cannot find it in the default theme. 485 FilePath LookupFallbackIcon(const std::string& icon_name) { 486 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 487 MimeUtilConstants::IconDirMtimeMap::iterator iter; 488 MimeUtilConstants::IconDirMtimeMap* icon_dirs = &constants->icon_dirs_; 489 MimeUtilConstants::IconFormats* icon_formats = &constants->icon_formats_; 490 for (iter = icon_dirs->begin(); iter != icon_dirs->end(); ++iter) { 491 for (size_t i = 0; i < icon_formats->size(); ++i) { 492 FilePath icon = iter->first.Append(icon_name + (*icon_formats)[i]); 493 if (PathExists(icon)) 494 return icon; 495 } 496 } 497 return FilePath(); 498 } 499 500 // Initialize the list of default themes. 501 void InitDefaultThemes() { 502 IconTheme** default_themes = 503 MimeUtilConstants::GetInstance()->default_themes_; 504 505 scoped_ptr<base::Environment> env(base::Environment::Create()); 506 base::nix::DesktopEnvironment desktop_env = 507 base::nix::GetDesktopEnvironment(env.get()); 508 if (desktop_env == base::nix::DESKTOP_ENVIRONMENT_KDE3 || 509 desktop_env == base::nix::DESKTOP_ENVIRONMENT_KDE4) { 510 // KDE 511 std::string kde_default_theme; 512 std::string kde_fallback_theme; 513 514 // TODO(thestig): Figure out how to get the current icon theme on KDE. 515 // Setting stored in ~/.kde/share/config/kdeglobals under Icons -> Theme. 516 default_themes[0] = NULL; 517 518 // Try some reasonable defaults for KDE. 519 if (desktop_env == base::nix::DESKTOP_ENVIRONMENT_KDE3) { 520 // KDE 3 521 kde_default_theme = "default.kde"; 522 kde_fallback_theme = "crystalsvg"; 523 } else { 524 // KDE 4 525 kde_default_theme = "default.kde4"; 526 kde_fallback_theme = "oxygen"; 527 } 528 default_themes[1] = IconTheme::LoadTheme(kde_default_theme); 529 default_themes[2] = IconTheme::LoadTheme(kde_fallback_theme); 530 } else { 531 // Assume it's Gnome and use GTK to figure out the theme. 532 default_themes[1] = IconTheme::LoadTheme( 533 MimeUtilConstants::GetInstance()->icon_theme_name_); 534 default_themes[2] = IconTheme::LoadTheme("gnome"); 535 } 536 // hicolor needs to be last per icon theme spec. 537 default_themes[3] = IconTheme::LoadTheme("hicolor"); 538 539 for (size_t i = 0; i < MimeUtilConstants::kDefaultThemeNum; i++) { 540 if (default_themes[i] == NULL) 541 continue; 542 // NULL out duplicate pointers. 543 for (size_t j = i + 1; j < MimeUtilConstants::kDefaultThemeNum; j++) { 544 if (default_themes[j] == default_themes[i]) 545 default_themes[j] = NULL; 546 } 547 } 548 } 549 550 // Try to find an icon with the name |icon_name| that's |size| pixels. 551 FilePath LookupIconInDefaultTheme(const std::string& icon_name, int size) { 552 EnsureUpdated(); 553 MimeUtilConstants* constants = MimeUtilConstants::GetInstance(); 554 MimeUtilConstants::IconThemeMap* icon_themes = &constants->icon_themes_; 555 if (icon_themes->empty()) 556 InitDefaultThemes(); 557 558 FilePath icon_path; 559 IconTheme** default_themes = constants->default_themes_; 560 for (size_t i = 0; i < MimeUtilConstants::kDefaultThemeNum; i++) { 561 if (default_themes[i]) { 562 icon_path = default_themes[i]->GetIconPath(icon_name, size, true); 563 if (!icon_path.empty()) 564 return icon_path; 565 } 566 } 567 return LookupFallbackIcon(icon_name); 568 } 569 570 MimeUtilConstants::~MimeUtilConstants() { 571 for (size_t i = 0; i < kDefaultThemeNum; i++) 572 delete default_themes_[i]; 573 } 574 575 } // namespace 576 577 std::string GetFileMimeType(const FilePath& filepath) { 578 if (filepath.empty()) 579 return std::string(); 580 base::ThreadRestrictions::AssertIOAllowed(); 581 base::AutoLock scoped_lock(g_mime_util_xdg_lock.Get()); 582 return xdg_mime_get_mime_type_from_file_name(filepath.value().c_str()); 583 } 584 585 std::string GetDataMimeType(const std::string& data) { 586 base::ThreadRestrictions::AssertIOAllowed(); 587 base::AutoLock scoped_lock(g_mime_util_xdg_lock.Get()); 588 return xdg_mime_get_mime_type_for_data(data.data(), data.length(), NULL); 589 } 590 591 void SetIconThemeName(const std::string& name) { 592 // If the theme name is already loaded, do nothing. Chrome doesn't respond 593 // to changes in the system theme, so we never need to set this more than 594 // once. 595 if (!MimeUtilConstants::GetInstance()->icon_theme_name_.empty()) 596 return; 597 598 MimeUtilConstants::GetInstance()->icon_theme_name_ = name; 599 } 600 601 FilePath GetMimeIcon(const std::string& mime_type, size_t size) { 602 base::ThreadRestrictions::AssertIOAllowed(); 603 std::vector<std::string> icon_names; 604 std::string icon_name; 605 FilePath icon_file; 606 607 if (!mime_type.empty()) { 608 base::AutoLock scoped_lock(g_mime_util_xdg_lock.Get()); 609 const char *icon = xdg_mime_get_icon(mime_type.c_str()); 610 icon_name = std::string(icon ? icon : ""); 611 } 612 613 if (icon_name.length()) 614 icon_names.push_back(icon_name); 615 616 // For text/plain, try text-plain. 617 icon_name = mime_type; 618 for (size_t i = icon_name.find('/', 0); i != std::string::npos; 619 i = icon_name.find('/', i + 1)) { 620 icon_name[i] = '-'; 621 } 622 icon_names.push_back(icon_name); 623 // Also try gnome-mime-text-plain. 624 icon_names.push_back("gnome-mime-" + icon_name); 625 626 // Try "deb" for "application/x-deb" in KDE 3. 627 size_t x_substr_pos = mime_type.find("/x-"); 628 if (x_substr_pos != std::string::npos) { 629 icon_name = mime_type.substr(x_substr_pos + 3); 630 icon_names.push_back(icon_name); 631 } 632 633 // Try generic name like text-x-generic. 634 icon_name = mime_type.substr(0, mime_type.find('/')) + "-x-generic"; 635 icon_names.push_back(icon_name); 636 637 // Last resort 638 icon_names.push_back("unknown"); 639 640 for (size_t i = 0; i < icon_names.size(); i++) { 641 if (icon_names[i][0] == '/') { 642 icon_file = FilePath(icon_names[i]); 643 if (PathExists(icon_file)) 644 return icon_file; 645 } else { 646 icon_file = LookupIconInDefaultTheme(icon_names[i], size); 647 if (!icon_file.empty()) 648 return icon_file; 649 } 650 } 651 return FilePath(); 652 } 653 654 } // namespace nix 655 } // namespace base 656