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