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