Serenity Operating System
1/*
2 * Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/Array.h>
9#include <AK/DeprecatedString.h>
10#include <AK/LexicalPath.h>
11#include <LibCore/ConfigFile.h>
12#include <LibCore/DeprecatedFile.h>
13#include <LibCore/MappedFile.h>
14#include <LibCore/StandardPaths.h>
15#include <LibELF/Image.h>
16#include <LibGUI/FileIconProvider.h>
17#include <LibGUI/Icon.h>
18#include <LibGUI/Painter.h>
19#include <LibGfx/Bitmap.h>
20#include <LibGfx/PNGLoader.h>
21#include <string.h>
22#include <sys/stat.h>
23#include <unistd.h>
24
25namespace GUI {
26
27static Icon s_hard_disk_icon;
28static Icon s_directory_icon;
29static Icon s_directory_open_icon;
30static Icon s_inaccessible_directory_icon;
31static Icon s_desktop_directory_icon;
32static Icon s_home_directory_icon;
33static Icon s_home_directory_open_icon;
34static Icon s_git_directory_icon;
35static Icon s_git_directory_open_icon;
36static Icon s_file_icon;
37static Icon s_symlink_icon;
38static Icon s_socket_icon;
39static Icon s_executable_icon;
40static Icon s_filetype_image_icon;
41static RefPtr<Gfx::Bitmap> s_symlink_emblem;
42static RefPtr<Gfx::Bitmap> s_symlink_emblem_small;
43
44static HashMap<DeprecatedString, Icon> s_filetype_icons;
45static HashMap<DeprecatedString, Vector<DeprecatedString>> s_filetype_patterns;
46
47static void initialize_executable_icon_if_needed()
48{
49 static bool initialized = false;
50 if (initialized)
51 return;
52 initialized = true;
53 s_executable_icon = Icon::default_icon("filetype-executable"sv);
54}
55
56static void initialize_filetype_image_icon_if_needed()
57{
58 static bool initialized = false;
59 if (initialized)
60 return;
61 initialized = true;
62 s_filetype_image_icon = Icon::default_icon("filetype-image"sv);
63}
64
65static void initialize_if_needed()
66{
67 static bool s_initialized = false;
68 if (s_initialized)
69 return;
70
71 auto config = Core::ConfigFile::open("/etc/FileIconProvider.ini").release_value_but_fixme_should_propagate_errors();
72
73 s_symlink_emblem = Gfx::Bitmap::load_from_file("/res/icons/symlink-emblem.png"sv).release_value_but_fixme_should_propagate_errors();
74 s_symlink_emblem_small = Gfx::Bitmap::load_from_file("/res/icons/symlink-emblem-small.png"sv).release_value_but_fixme_should_propagate_errors();
75
76 s_hard_disk_icon = Icon::default_icon("hard-disk"sv);
77 s_directory_icon = Icon::default_icon("filetype-folder"sv);
78 s_directory_open_icon = Icon::default_icon("filetype-folder-open"sv);
79 s_inaccessible_directory_icon = Icon::default_icon("filetype-folder-inaccessible"sv);
80 s_home_directory_icon = Icon::default_icon("home-directory"sv);
81 s_home_directory_open_icon = Icon::default_icon("home-directory-open"sv);
82 s_git_directory_icon = Icon::default_icon("git-directory"sv);
83 s_git_directory_open_icon = Icon::default_icon("git-directory-open"sv);
84 s_desktop_directory_icon = Icon::default_icon("desktop"sv);
85 s_file_icon = Icon::default_icon("filetype-unknown"sv);
86 s_symlink_icon = Icon::default_icon("filetype-symlink"sv);
87 s_socket_icon = Icon::default_icon("filetype-socket"sv);
88
89 initialize_filetype_image_icon_if_needed();
90 initialize_executable_icon_if_needed();
91
92 for (auto& filetype : config->keys("Icons")) {
93 s_filetype_icons.set(filetype, Icon::default_icon(DeprecatedString::formatted("filetype-{}", filetype)));
94 s_filetype_patterns.set(filetype, config->read_entry("Icons", filetype).split(','));
95 }
96
97 s_initialized = true;
98}
99
100Icon FileIconProvider::directory_icon()
101{
102 initialize_if_needed();
103 return s_directory_icon;
104}
105
106Icon FileIconProvider::directory_open_icon()
107{
108 initialize_if_needed();
109 return s_directory_open_icon;
110}
111
112Icon FileIconProvider::home_directory_icon()
113{
114 initialize_if_needed();
115 return s_home_directory_icon;
116}
117
118Icon FileIconProvider::desktop_directory_icon()
119{
120 initialize_if_needed();
121 return s_desktop_directory_icon;
122}
123
124Icon FileIconProvider::home_directory_open_icon()
125{
126 initialize_if_needed();
127 return s_home_directory_open_icon;
128}
129
130Icon FileIconProvider::git_directory_icon()
131{
132 initialize_if_needed();
133 return s_git_directory_icon;
134}
135
136Icon FileIconProvider::git_directory_open_icon()
137{
138 initialize_if_needed();
139 return s_git_directory_open_icon;
140}
141
142Icon FileIconProvider::filetype_image_icon()
143{
144 initialize_filetype_image_icon_if_needed();
145 return s_filetype_image_icon;
146}
147
148Icon FileIconProvider::icon_for_path(DeprecatedString const& path)
149{
150 struct stat stat;
151 if (::stat(path.characters(), &stat) < 0)
152 return s_file_icon;
153 return icon_for_path(path, stat.st_mode);
154}
155
156Icon FileIconProvider::icon_for_executable(DeprecatedString const& path)
157{
158 static HashMap<DeprecatedString, Icon> app_icon_cache;
159
160 if (auto it = app_icon_cache.find(path); it != app_icon_cache.end())
161 return it->value;
162
163 initialize_executable_icon_if_needed();
164
165 // If the icon for an app isn't in the cache we attempt to load the file as an ELF image and extract
166 // the serenity_app_icon_* sections which should contain the icons as raw PNG data. In the future it would
167 // be better if the binary signalled the image format being used or we deduced it, e.g. using magic bytes.
168 auto file_or_error = Core::MappedFile::map(path);
169 if (file_or_error.is_error()) {
170 app_icon_cache.set(path, s_executable_icon);
171 return s_executable_icon;
172 }
173
174 auto& mapped_file = file_or_error.value();
175
176 if (mapped_file->size() < SELFMAG) {
177 app_icon_cache.set(path, s_executable_icon);
178 return s_executable_icon;
179 }
180
181 if (memcmp(mapped_file->data(), ELFMAG, SELFMAG) != 0) {
182 app_icon_cache.set(path, s_executable_icon);
183 return s_executable_icon;
184 }
185
186 auto image = ELF::Image((u8 const*)mapped_file->data(), mapped_file->size());
187 if (!image.is_valid()) {
188 app_icon_cache.set(path, s_executable_icon);
189 return s_executable_icon;
190 }
191
192 // If any of the required sections are missing then use the defaults
193 Icon icon;
194 struct IconSection {
195 StringView section_name;
196 int image_size;
197 };
198
199 static constexpr Array<IconSection, 2> icon_sections = {
200 IconSection { .section_name = "serenity_icon_s"sv, .image_size = 16 },
201 IconSection { .section_name = "serenity_icon_m"sv, .image_size = 32 }
202 };
203
204 bool had_error = false;
205 for (auto const& icon_section : icon_sections) {
206 auto section = image.lookup_section(icon_section.section_name);
207
208 RefPtr<Gfx::Bitmap const> bitmap;
209 if (!section.has_value()) {
210 bitmap = s_executable_icon.bitmap_for_size(icon_section.image_size);
211 } else {
212 // FIXME: Use the ImageDecoder service.
213 if (Gfx::PNGImageDecoderPlugin::sniff({ section->raw_data(), section->size() })) {
214 auto png_decoder = Gfx::PNGImageDecoderPlugin::create({ section->raw_data(), section->size() }).release_value_but_fixme_should_propagate_errors();
215 if (png_decoder->initialize()) {
216 auto frame_or_error = png_decoder->frame(0);
217 if (!frame_or_error.is_error()) {
218 bitmap = frame_or_error.value().image;
219 }
220 }
221 }
222 }
223
224 if (!bitmap) {
225 dbgln("Failed to find embedded icon and failed to clone default icon for application {} at icon size {}", path, icon_section.image_size);
226 had_error = true;
227 continue;
228 }
229
230 icon.set_bitmap_for_size(icon_section.image_size, move(bitmap));
231 }
232
233 if (had_error) {
234 app_icon_cache.set(path, s_executable_icon);
235 return s_executable_icon;
236 }
237 app_icon_cache.set(path, icon);
238 return icon;
239}
240
241Icon FileIconProvider::icon_for_path(DeprecatedString const& path, mode_t mode)
242{
243 initialize_if_needed();
244 if (path == "/")
245 return s_hard_disk_icon;
246 if (S_ISDIR(mode)) {
247 if (path == Core::StandardPaths::home_directory())
248 return s_home_directory_icon;
249 if (path == Core::StandardPaths::desktop_directory())
250 return s_desktop_directory_icon;
251 if (access(path.characters(), R_OK | X_OK) < 0)
252 return s_inaccessible_directory_icon;
253 if (path.ends_with(".git"sv))
254 return s_git_directory_icon;
255 return s_directory_icon;
256 }
257 if (S_ISLNK(mode)) {
258 auto raw_symlink_target_or_error = Core::DeprecatedFile::read_link(path);
259 if (raw_symlink_target_or_error.is_error())
260 return s_symlink_icon;
261
262 auto raw_symlink_target = raw_symlink_target_or_error.release_value();
263 if (raw_symlink_target.is_null())
264 return s_symlink_icon;
265
266 DeprecatedString target_path;
267 if (raw_symlink_target.starts_with('/')) {
268 target_path = raw_symlink_target;
269 } else {
270 target_path = Core::DeprecatedFile::real_path_for(DeprecatedString::formatted("{}/{}", LexicalPath::dirname(path), raw_symlink_target));
271 }
272 auto target_icon = icon_for_path(target_path);
273
274 Icon generated_icon;
275 for (auto size : target_icon.sizes()) {
276 auto& emblem = size < 32 ? *s_symlink_emblem_small : *s_symlink_emblem;
277 auto original_bitmap = target_icon.bitmap_for_size(size);
278 VERIFY(original_bitmap);
279 auto generated_bitmap_or_error = original_bitmap->clone();
280 if (generated_bitmap_or_error.is_error()) {
281 dbgln("Failed to clone {}x{} icon for symlink variant", size, size);
282 return s_symlink_icon;
283 }
284 auto generated_bitmap = generated_bitmap_or_error.release_value_but_fixme_should_propagate_errors();
285 GUI::Painter painter(*generated_bitmap);
286 painter.blit({ size - emblem.width(), size - emblem.height() }, emblem, emblem.rect());
287
288 generated_icon.set_bitmap_for_size(size, move(generated_bitmap));
289 }
290 return generated_icon;
291 }
292 if (S_ISSOCK(mode))
293 return s_socket_icon;
294
295 if (mode & (S_IXUSR | S_IXGRP | S_IXOTH))
296 return icon_for_executable(path);
297
298 if (Gfx::Bitmap::is_path_a_supported_image_format(path.view()))
299 return s_filetype_image_icon;
300
301 for (auto& filetype : s_filetype_icons.keys()) {
302 auto pattern_it = s_filetype_patterns.find(filetype);
303 if (pattern_it == s_filetype_patterns.end())
304 continue;
305 for (auto& pattern : pattern_it->value) {
306 if (path.matches(pattern, CaseSensitivity::CaseInsensitive))
307 return s_filetype_icons.get(filetype).value();
308 }
309 }
310
311 return s_file_icon;
312}
313
314}