Serenity Operating System
1/*
2 * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#define AK_DONT_REPLACE_STD
8
9#include <AK/DeprecatedString.h>
10#include <AK/LexicalPath.h>
11#include <AK/Platform.h>
12#include <AK/ScopeGuard.h>
13#include <LibArchive/Tar.h>
14#include <LibArchive/TarStream.h>
15#include <LibCore/Directory.h>
16#include <LibCore/FileStream.h>
17#include <LibCore/System.h>
18#include <LibMain/Main.h>
19
20#include <QCoreApplication>
21#include <QJniObject>
22#include <QSslSocket>
23
24#ifndef AK_OS_ANDROID
25# error This file is for Android only, check CMake config!
26#endif
27
28// HACK ALERT, we need to include LibMain manually here because the Qt build system doesn't include LibMain.a in the actual executable,
29// nor include it in libladybird_<arch>.so
30#include <LibMain/Main.cpp> // NOLINT(bugprone-suspicious-include)
31
32extern DeprecatedString s_serenity_resource_root;
33
34void android_platform_init();
35static void extract_ladybird_resources();
36static ErrorOr<void> extract_tar_archive(DeprecatedString archive_file, DeprecatedString output_directory);
37
38void android_platform_init()
39{
40 qDebug() << "Device supports OpenSSL: " << QSslSocket::supportsSsl();
41
42 QJniObject res = QJniObject::callStaticMethod<jstring>("org/serenityos/ladybird/TransferAssets",
43 "transferAssets",
44 "(Landroid/content/Context;)Ljava/lang/String;",
45 QNativeInterface::QAndroidApplication::context());
46 s_serenity_resource_root = res.toString().toUtf8().data();
47
48 extract_ladybird_resources();
49}
50
51void extract_ladybird_resources()
52{
53 qDebug() << "serenity resource root is " << s_serenity_resource_root.characters();
54 auto file_or_error = Core::System::open(DeprecatedString::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root), O_RDONLY);
55 if (file_or_error.is_error()) {
56 qDebug() << "Unable to open test file file as expected, extracting asssets...";
57
58 MUST(extract_tar_archive(DeprecatedString::formatted("{}/ladybird-assets.tar", s_serenity_resource_root), s_serenity_resource_root));
59 } else {
60 qDebug() << "Opened app-browser.png test file, good to go!";
61 qDebug() << "Hopefully no developer changed the asset files and expected them to be re-extracted!";
62 }
63}
64
65ErrorOr<void> extract_tar_archive(DeprecatedString archive_file, DeprecatedString output_directory)
66{
67 constexpr size_t buffer_size = 4096;
68
69 auto file = TRY(Core::DeprecatedFile::open(archive_file, Core::OpenMode::ReadOnly));
70
71 DeprecatedString old_pwd = TRY(Core::System::getcwd());
72
73 TRY(Core::System::chdir(output_directory));
74 ScopeGuard go_back = [&old_pwd] { MUST(Core::System::chdir(old_pwd)); };
75
76 Core::InputFileStream file_stream(file);
77
78 Archive::TarInputStream tar_stream(file_stream);
79 if (!tar_stream.valid()) {
80 qDebug() << "the provided file is not a well-formatted ustar file";
81 return Error::from_errno(EINVAL);
82 }
83
84 HashMap<DeprecatedString, DeprecatedString> global_overrides;
85 HashMap<DeprecatedString, DeprecatedString> local_overrides;
86
87 auto get_override = [&](StringView key) -> Optional<DeprecatedString> {
88 Optional<DeprecatedString> maybe_local = local_overrides.get(key);
89
90 if (maybe_local.has_value())
91 return maybe_local;
92
93 Optional<DeprecatedString> maybe_global = global_overrides.get(key);
94
95 if (maybe_global.has_value())
96 return maybe_global;
97
98 return {};
99 };
100
101 for (; !tar_stream.finished(); tar_stream.advance()) {
102 Archive::TarFileHeader const& header = tar_stream.header();
103
104 // Handle meta-entries earlier to avoid consuming the file content stream.
105 if (header.content_is_like_extended_header()) {
106 switch (header.type_flag()) {
107 case Archive::TarFileType::GlobalExtendedHeader: {
108 TRY(tar_stream.for_each_extended_header([&](StringView key, StringView value) {
109 if (value.length() == 0)
110 global_overrides.remove(key);
111 else
112 global_overrides.set(key, value);
113 }));
114 break;
115 }
116 case Archive::TarFileType::ExtendedHeader: {
117 TRY(tar_stream.for_each_extended_header([&](StringView key, StringView value) {
118 local_overrides.set(key, value);
119 }));
120 break;
121 }
122 default:
123 warnln("Unknown extended header type '{}' of {}", (char)header.type_flag(), header.filename());
124 VERIFY_NOT_REACHED();
125 }
126
127 continue;
128 }
129
130 Archive::TarFileStream file_stream = tar_stream.file_contents();
131
132 // Handle other header types that don't just have an effect on extraction.
133 switch (header.type_flag()) {
134 case Archive::TarFileType::LongName: {
135 StringBuilder long_name;
136
137 Array<u8, buffer_size> buffer;
138 size_t bytes_read;
139
140 while ((bytes_read = file_stream.read(buffer)) > 0)
141 long_name.append(reinterpret_cast<char*>(buffer.data()), bytes_read);
142
143 local_overrides.set("path", long_name.to_deprecated_string());
144 continue;
145 }
146 default:
147 // None of the relevant headers, so continue as normal.
148 break;
149 }
150
151 LexicalPath path = LexicalPath(header.filename());
152 if (!header.prefix().is_empty())
153 path = path.prepend(header.prefix());
154 DeprecatedString filename = get_override("path"sv).value_or(path.string());
155
156 DeprecatedString absolute_path = Core::DeprecatedFile::absolute_path(filename);
157 auto parent_path = LexicalPath(absolute_path).parent();
158
159 switch (header.type_flag()) {
160 case Archive::TarFileType::NormalFile:
161 case Archive::TarFileType::AlternateNormalFile: {
162 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
163
164 int fd = TRY(Core::System::open(absolute_path, O_CREAT | O_WRONLY, header.mode()));
165
166 Array<u8, buffer_size> buffer;
167 size_t bytes_read;
168 while ((bytes_read = file_stream.read(buffer)) > 0)
169 TRY(Core::System::write(fd, buffer.span().slice(0, bytes_read)));
170
171 TRY(Core::System::close(fd));
172 break;
173 }
174 case Archive::TarFileType::SymLink: {
175 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
176
177 TRY(Core::System::symlink(header.link_name(), absolute_path));
178 break;
179 }
180 case Archive::TarFileType::Directory: {
181 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
182
183 auto result_or_error = Core::System::mkdir(absolute_path, header.mode());
184 if (result_or_error.is_error() && result_or_error.error().code() != EEXIST)
185 return result_or_error.error();
186 break;
187 }
188 default:
189 // FIXME: Implement other file types
190 warnln("file type '{}' of {} is not yet supported", (char)header.type_flag(), header.filename());
191 VERIFY_NOT_REACHED();
192 }
193
194 // Non-global headers should be cleared after every file.
195 local_overrides.clear();
196 }
197 file_stream.close();
198
199 return {};
200}