Serenity Operating System
1/*
2 * Copyright (c) 2021, Valtteri Koskivuori <vkoskiv@gmail.com>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Vector.h>
8#include <LibCompress/Gzip.h>
9#include <LibCore/ArgsParser.h>
10#include <LibCore/File.h>
11#include <LibCore/MappedFile.h>
12#include <LibCore/MimeData.h>
13#include <LibCore/System.h>
14#include <LibELF/Image.h>
15#include <LibELF/Validation.h>
16#include <LibGfx/ImageDecoder.h>
17#include <LibMain/Main.h>
18#include <stdio.h>
19#include <sys/stat.h>
20#include <unistd.h>
21
22static Optional<DeprecatedString> description_only(StringView description, StringView)
23{
24 return description;
25}
26
27// FIXME: Ideally Gfx::ImageDecoder could tell us the image type directly.
28static Optional<DeprecatedString> image_details(StringView description, StringView path)
29{
30 auto file_or_error = Core::MappedFile::map(path);
31 if (file_or_error.is_error())
32 return {};
33
34 auto& mapped_file = *file_or_error.value();
35 auto mime_type = Core::guess_mime_type_based_on_filename(path);
36 auto image_decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(mapped_file.bytes(), mime_type);
37 if (!image_decoder)
38 return {};
39
40 StringBuilder builder;
41 builder.appendff("{}, {} x {}", description, image_decoder->width(), image_decoder->height());
42 if (image_decoder->is_animated()) {
43 builder.appendff(", animated with {} frames that loop", image_decoder->frame_count());
44 int loop_count = image_decoder->loop_count();
45 if (loop_count == 0)
46 builder.appendff(" indefinitely");
47 else
48 builder.appendff(" {} {}", loop_count, loop_count == 1 ? "time" : "times");
49 }
50
51 return builder.to_deprecated_string();
52}
53
54static Optional<DeprecatedString> gzip_details(StringView description, StringView path)
55{
56 auto file_or_error = Core::MappedFile::map(path);
57 if (file_or_error.is_error())
58 return {};
59
60 auto& mapped_file = *file_or_error.value();
61 if (!Compress::GzipDecompressor::is_likely_compressed(mapped_file.bytes()))
62 return {};
63
64 auto gzip_details = Compress::GzipDecompressor::describe_header(mapped_file.bytes());
65 if (!gzip_details.has_value())
66 return {};
67
68 return DeprecatedString::formatted("{}, {}", description, gzip_details.value());
69}
70
71static Optional<DeprecatedString> elf_details(StringView description, StringView path)
72{
73 auto file_or_error = Core::MappedFile::map(path);
74 if (file_or_error.is_error())
75 return {};
76 auto& mapped_file = *file_or_error.value();
77 auto elf_data = mapped_file.bytes();
78 ELF::Image elf_image(elf_data);
79 if (!elf_image.is_valid())
80 return {};
81
82 StringBuilder interpreter_path_builder;
83 auto result_or_error = ELF::validate_program_headers(*(const ElfW(Ehdr)*)elf_data.data(), elf_data.size(), elf_data, &interpreter_path_builder);
84 if (result_or_error.is_error() || !result_or_error.value())
85 return {};
86 auto interpreter_path = interpreter_path_builder.string_view();
87
88 auto& header = *reinterpret_cast<const ElfW(Ehdr)*>(elf_data.data());
89
90 auto bitness = header.e_ident[EI_CLASS] == ELFCLASS64 ? "64" : "32";
91 auto byteorder = header.e_ident[EI_DATA] == ELFDATA2LSB ? "LSB" : "MSB";
92
93 bool is_dynamically_linked = !interpreter_path.is_empty();
94 DeprecatedString dynamic_section = DeprecatedString::formatted(", dynamically linked, interpreter {}", interpreter_path);
95
96 return DeprecatedString::formatted("{} {}-bit {} {}, {}, version {} ({}){}",
97 description,
98 bitness,
99 byteorder,
100 ELF::Image::object_file_type_to_string(header.e_type).value_or("(?)"sv),
101 ELF::Image::object_machine_type_to_string(header.e_machine).value_or("(?)"sv),
102 header.e_ident[EI_ABIVERSION],
103 ELF::Image::object_abi_type_to_string(header.e_ident[EI_OSABI]).value_or("(?)"sv),
104 is_dynamically_linked ? dynamic_section : "");
105}
106
107#define ENUMERATE_MIME_TYPE_DESCRIPTIONS \
108 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/gzip"sv, "gzip compressed data"sv, gzip_details) \
109 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/javascript"sv, "JavaScript source"sv, description_only) \
110 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/json"sv, "JSON data"sv, description_only) \
111 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/pdf"sv, "PDF document"sv, description_only) \
112 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/rtf"sv, "Rich text file"sv, description_only) \
113 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/tar"sv, "tape archive"sv, description_only) \
114 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/wasm"sv, "WebAssembly bytecode"sv, description_only) \
115 __ENUMERATE_MIME_TYPE_DESCRIPTION("application/x-7z-compressed"sv, "7-Zip archive"sv, description_only) \
116 __ENUMERATE_MIME_TYPE_DESCRIPTION("audio/flac"sv, "FLAC audio"sv, description_only) \
117 __ENUMERATE_MIME_TYPE_DESCRIPTION("audio/midi"sv, "MIDI notes"sv, description_only) \
118 __ENUMERATE_MIME_TYPE_DESCRIPTION("audio/mpeg"sv, "MP3 audio"sv, description_only) \
119 __ENUMERATE_MIME_TYPE_DESCRIPTION("audio/qoa"sv, "Quite OK Audio"sv, description_only) \
120 __ENUMERATE_MIME_TYPE_DESCRIPTION("audio/wave"sv, "WAVE audio"sv, description_only) \
121 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/blender"sv, "Blender project file"sv, description_only) \
122 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/elf"sv, "ELF"sv, elf_details) \
123 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/ext"sv, "ext filesystem"sv, description_only) \
124 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/iso-9660"sv, "ISO 9660 CD/DVD image"sv, description_only) \
125 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/isz"sv, "Compressed ISO image"sv, description_only) \
126 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/lua-bytecode"sv, "Lua bytecode"sv, description_only) \
127 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/matroska"sv, "Matroska container"sv, description_only) \
128 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/nes-rom"sv, "Nintendo Entertainment System ROM"sv, description_only) \
129 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/qcow"sv, "qcow file"sv, description_only) \
130 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/raw-zlib"sv, "raw zlib stream"sv, description_only) \
131 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/sqlite"sv, "sqlite database"sv, description_only) \
132 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/win-31x-compressed"sv, "Windows 3.1X compressed file"sv, description_only) \
133 __ENUMERATE_MIME_TYPE_DESCRIPTION("extra/win-95-compressed"sv, "Windows 95 compressed file"sv, description_only) \
134 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/bmp"sv, "BMP image data"sv, image_details) \
135 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/gif"sv, "GIF image data"sv, image_details) \
136 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/jpeg"sv, "JPEG image data"sv, image_details) \
137 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/png"sv, "PNG image data"sv, image_details) \
138 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/webp"sv, "WebP image data"sv, image_details) \
139 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/x-portable-bitmap"sv, "PBM image data"sv, image_details) \
140 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/x-portable-graymap"sv, "PGM image data"sv, image_details) \
141 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/x-portable-pixmap"sv, "PPM image data"sv, image_details) \
142 __ENUMERATE_MIME_TYPE_DESCRIPTION("image/x-qoi"sv, "QOI image data"sv, image_details) \
143 __ENUMERATE_MIME_TYPE_DESCRIPTION("text/markdown"sv, "Markdown document"sv, description_only) \
144 __ENUMERATE_MIME_TYPE_DESCRIPTION("text/x-shellscript"sv, "POSIX shell script text executable"sv, description_only)
145
146static Optional<DeprecatedString> get_description_from_mime_type(StringView mime, StringView path)
147{
148#define __ENUMERATE_MIME_TYPE_DESCRIPTION(mime_type, description, details) \
149 if (mime_type == mime) \
150 return details(description, path);
151 ENUMERATE_MIME_TYPE_DESCRIPTIONS;
152#undef __ENUMERATE_MIME_TYPE_DESCRIPTION
153 return {};
154}
155
156ErrorOr<int> serenity_main(Main::Arguments arguments)
157{
158 TRY(Core::System::pledge("stdio rpath"));
159
160 Vector<StringView> paths;
161 bool flag_mime_only = false;
162
163 Core::ArgsParser args_parser;
164 args_parser.set_general_help("Determine type of files");
165 args_parser.add_option(flag_mime_only, "Only print mime type", "mime-type", 'I');
166 args_parser.add_positional_argument(paths, "Files to identify", "files", Core::ArgsParser::Required::Yes);
167 args_parser.parse(arguments);
168
169 bool all_ok = true;
170 // Read accounts for longest possible offset + signature we currently match against.
171 auto buffer = TRY(ByteBuffer::create_uninitialized(0x9006));
172
173 for (auto const& path : paths) {
174 auto file_or_error = Core::File::open(path, Core::File::OpenMode::Read);
175 if (file_or_error.is_error()) {
176 warnln("{}: {}", path, file_or_error.release_error());
177 all_ok = false;
178 continue;
179 }
180 auto file = file_or_error.release_value();
181
182 struct stat file_stat = TRY(Core::System::lstat(path));
183
184 auto file_size_in_bytes = file_stat.st_size;
185 if (S_ISDIR(file_stat.st_mode)) {
186 outln("{}: directory", path);
187 } else if (!file_size_in_bytes) {
188 outln("{}: empty", path);
189 } else {
190 auto bytes = TRY(file->read_some(buffer));
191 auto file_name_guess = Core::guess_mime_type_based_on_filename(path);
192 auto mime_type = Core::guess_mime_type_based_on_sniffed_bytes(bytes).value_or(file_name_guess);
193 auto human_readable_description = get_description_from_mime_type(mime_type, path).value_or(mime_type);
194 outln("{}: {}", path, flag_mime_only ? mime_type : human_readable_description);
195 }
196 }
197
198 return all_ok ? 0 : 1;
199}