Serenity Operating System
at master 314 lines 11 kB view raw
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}