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