- add sources.
[platform/framework/web/crosswalk.git] / src / base / nix / mime_util_xdg.cc
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