Cogs.Core
IO.Emscripten.cpp
1#include "IO.h"
2
3#include "FileContents.h"
4
5#include "../Logging/Logger.h"
6#include "../Collections/SmallVector.h"
7
8#include <emscripten/emscripten.h>
9#include <emscripten/fetch.h>
10#include <cinttypes>
11
12namespace {
13 Cogs::Logging::Log logger = Cogs::Logging::getLogger("EmscriptenIO");
14
15 struct MallocedFileContents : public Cogs::FileContents {
16 std::string _origin;
17
18 MallocedFileContents(const uint8_t* ptr, size_t len, const std::string& origin, Cogs::FileContentsHints hints) : FileContents(ptr, len, hints), _origin(origin) {}
19
20 ~MallocedFileContents() {
21 EM_ASM({
22 _free($0);
23 }, ptr);
24 }
25 Cogs::Memory::MemoryBuffer take() override { return takeCopy(); }
26 Cogs::StringView origin() override { return _origin; }
27 };
28
29 struct FetchBackedFileContents : public Cogs::FileContents {
30 emscripten_fetch_t* fetch = nullptr;
31
32 FetchBackedFileContents(emscripten_fetch_t* fetch, Cogs::FileContentsHints hints) : FileContents((const uint8_t*)fetch->data, fetch->numBytes, hints), fetch(fetch) {}
33
34 ~FetchBackedFileContents() {
35 if (fetch) {
36 emscripten_fetch_close(fetch);
37 }
38 }
39
40 Cogs::Memory::MemoryBuffer take() override { return takeCopy(); }
41 Cogs::StringView origin() override { return fetch->url; }
42 };
43
44 struct UserData {
45 std::string name;
46 Cogs::FileContents::Callback callback;
48 };
49
50 // Signature of fetch-handler provided by client.
51 // ----------------------------------------------
52 //
53 // offset == 0 and size == 0 means read as much as possible.
54 // Otherwise, try to read the specified range.
55 // offset != 0 and size == 0 is an error and is supposed to get
56 // caught before fetch handler is invoked, i.e. it is OK to
57 // assert on that.
58 void(*EmscriptenIO_fetchHandler)(const char*, UserData*, double offset, double size) = nullptr;
59
60 void downloadSucceeded(emscripten_fetch_t *fetch) {
61 std::unique_ptr<UserData> userData(reinterpret_cast<UserData *>(fetch->userData));
62
63 LOG_TRACE(logger, "Fetched %s (%llu bytes)", fetch->url, fetch->numBytes);
64
65 auto contents = std::make_unique<FetchBackedFileContents>(fetch, userData->hints);
66 userData->callback(std::move(contents));
67 }
68
69 void downloadFailed(emscripten_fetch_t *fetch) {
70 std::unique_ptr<UserData> userData(reinterpret_cast<UserData *>(fetch->userData));
71
72 LOG_ERROR(logger, "Failed to fetch %s (error=%d, status=%s)", fetch->url, fetch->status, fetch->statusText);
73
74 emscripten_fetch_close(fetch);
75 userData->callback(std::unique_ptr<Cogs::FileContents>());
76 }
77
78 extern "C" {
79 EMSCRIPTEN_KEEPALIVE void registerFetchHandler(void* handler_callback)
80 {
81 EmscriptenIO_fetchHandler = reinterpret_cast<void(*)(const char*, UserData*, double offset, double size)>(handler_callback);
82 LOG_DEBUG(logger, "Client fetch handler set (function=%zu)", size_t(EmscriptenIO_fetchHandler));
83 }
84
85 EMSCRIPTEN_KEEPALIVE void handleFetchSuccess(void * /*ctx*/, const uint8_t* data, size_t len, UserData* userData)
86 {
87 LOG_TRACE(logger, "Fetched %s (data=%#zX, len=%zu)", userData->name.c_str(), size_t(data), len);
88 auto contents = std::make_unique<MallocedFileContents>(data, len, userData->name, userData->hints);
89 userData->callback(std::move(contents));
90 delete userData;
91 }
92
93 EMSCRIPTEN_KEEPALIVE void handleFetchFailure(void * /*ctx*/, UserData* userData)
94 {
95 LOG_ERROR(logger, "Failed to fetch %s", userData->name.c_str());
96 userData->callback(std::unique_ptr<Cogs::FileContents>());
97 delete userData;
98 }
99 }
100}
101
102void Cogs::IO::readFileAsync(const std::string & path, const FileContents::Callback& callback, uint64_t offset, uint64_t size, FileContentsHints hints)
103{
104 if(size == 0 && offset != 0) {
105 LOG_ERROR(logger, "readFileAsync: Illegal range, trying to read zero bytes from offset (%" PRIu64 ")", offset);
106 callback(std::unique_ptr<Cogs::FileContents>());
107 return;
108 }
109
110 auto userData = new UserData{ path, callback, hints };
111
112 if (EmscriptenIO_fetchHandler) {
113 EmscriptenIO_fetchHandler(path.c_str(), userData, offset, size);
114 }
115
116 else {
117 // uint64_t is max 20, max length is 6(bytes=) + 20 + 1(-) + 20 + 1(/0) = 48.
118 char rangeBuffer[64];
120
121 if(size != 0) {
122 // add headers {"Range", "bytes=<start>-<stop>"};
123 int n = snprintf(rangeBuffer, sizeof(rangeBuffer),
124 "bytes=%" PRIu64 "-%" PRIu64,
125 offset, offset + std::max(uint64_t(1), size) - 1);
126 if( (n < 0) || ( sizeof(rangeBuffer) <= static_cast<size_t>(n)) ) {
127 LOG_ERROR(logger, "snprintf failed for: bytes=%" PRIu64 "-%" PRIu64, offset, offset + std::max(uint64_t(1), size) - 1);
128 callback(std::unique_ptr<Cogs::FileContents>());
129 return;
130 }
131 headers.push_back("Range");
132 headers.push_back(rangeBuffer);
133 headers.push_back(nullptr);
134 }
135
136 emscripten_fetch_attr_t attr;
137 emscripten_fetch_attr_init(&attr);
138 attr.userData = userData;
139 strcpy(attr.requestMethod, "GET");
140 attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
141 attr.onsuccess = downloadSucceeded;
142 attr.onerror = downloadFailed;
143 if(0 < headers.size()) {
144 // header strings get strdup'd in emscripten/system/lib/fetch/emscripten_fetch.cpp
145 attr.requestHeaders = headers.data();
146 }
147 emscripten_fetch(&attr, path.c_str());
148 }
149}
150
151Cogs::FileContents::Ptr Cogs::IO::readFileSync(const std::string& /*fileName*/, uint64_t /*offset*/, uint64_t /*size*/, FileContentsHints /*hints*/) {
152 assert(false && "Should not happen");
153 return {};
154}
155
156bool Cogs::IO::readTextFile(std::string& /*content*/, const std::string& /*fileName*/) {
157 return false;
158}
159
160void Cogs::IO::autoExpandEnvironmentVariables(std::string& /*text*/) {
161}
162
163std::string Cogs::IO::expandEnvironmentVariables(const std::string& input) {
164 return input;
165}
166
167std::string Cogs::IO::expandEnvironmentVariable(const std::string& /*name*/) {
168 return std::string();
169}
170
171char Cogs::IO::pathSeparator() {
172 return '/';
173}
174
175bool Cogs::IO::exists(std::string_view /*path*/) {
176 return false;
177}
178
179bool Cogs::IO::isFile(std::string_view /*path*/) {
180 return false;
181}
182
183bool Cogs::IO::isDirectory(std::string_view /*path*/) {
184 return false;
185}
186
187std::string Cogs::IO::absolute(std::string_view path) {
188 return std::string(path);
189}
190
191std::string Cogs::IO::canonical(std::string_view path) {
192 return std::string(path);
193}
194
195bool Cogs::IO::remove(std::string_view /*path*/) {
196 return false;
197}
198
199std::string Cogs::IO::combine(std::string_view path, std::string_view extensionPath) {
200 if (path.empty()) return std::string(extensionPath);
201
202 // For some urls with parameter list where we dont want / between search path and resource name. Eg. service?fileName=resourceName
203 std::string combined;
204 combined.reserve(path.length() + extensionPath.length() + 1U);
205
206 combined += path;
207 if (path.back() != '=') {
208 combined += '/';
209 }
210 combined += extensionPath;
211
212 const std::string sep = "://";
213 size_t sepI = combined.find(sep);
214 sepI = (sepI == std::string::npos) ? 0 : sepI + sep.size();
215
216 std::string prefix = combined.substr(0, sepI);
217 std::string body = combined.substr(sepI);
218
219 replaceAll(body, "\\", "/");
220
221 size_t newSize = 0;
222 for (size_t i = 0, n = body.size(); i < n; i++) {
223 body[newSize++] = body[i];
224
225 // Skip consecutive '/': "a///b/c///d//" => "a/b/c/d/"
226 while (i + 1 < n && body[i] == '/' && body[i + 1] == '/') i++;
227 }
228 body.resize(newSize);
229
230 return prefix + body;
231}
232
233std::string Cogs::IO::expandPath(std::string_view /*fileName*/) {
234 return std::string();
235}
236
237std::string Cogs::IO::parentPath(std::string_view str) {
238 std::string path(str);
239
240 replaceAll(path, "\\", "/");
241 return path.substr(0, path.find_last_of('/'));
242}
243
244std::string Cogs::IO::relativePath(std::string_view str) {
245 return std::string(str);
246}
247
248std::string Cogs::IO::relativePath(std::string_view str, std::string_view /*base*/) {
249 return std::string(str);
250}
251
252std::string Cogs::IO::getPathToExe() {
253 assert(false); // TODO
254 return std::string();
255}
256
257std::string Cogs::IO::getPathRelativeToDll(const std::string& /*fileName*/, void* /*handle*/) {
258 assert(false); // TODO
259 return std::string();
260}
261
262std::string Cogs::IO::getTempPath() {
263 return std::string();
264}
265
266std::string Cogs::IO::getAppDataPath() {
267 return "./";
268}
269
270std::string Cogs::IO::getOrgLocalDataPath() {
271 return "./";
272}
273
274bool Cogs::IO::isAbsolute(std::string_view str) {
275 // If separator after URL scheme is present, assume it is an absolute path, otherwise assume relative.
276 return str.find("://") != std::string_view::npos;
277}
278
279bool Cogs::IO::isRelative(std::string_view str) {
280 // If separator after URL scheme is present, assume it is an absolute path, otherwise assume relative.
281 return str.find("://") == std::string_view::npos;
282}
283
284std::string Cogs::IO::fileName(std::string_view str) {
285 size_t lastSep = str.find_last_of("/\\");
286 return lastSep == std::string_view::npos ? std::string(str) : std::string(str.substr(lastSep + 1U));
287}
288
289std::string Cogs::IO::extension(std::string_view str) {
290 size_t found = str.find_last_of('.');
291 size_t lastSep = str.find_last_of("/\\");
292 if (found == std::string_view::npos) {
293 return std::string();
294 }
295 else if (lastSep != std::string_view::npos && lastSep > found) {
296 return std::string();
297 }
298 return std::string(str.substr(found));
299}
300
301std::string Cogs::IO::stem(std::string_view str) {
302 std::string fn = Cogs::IO::fileName(str);
303 std::string ext = Cogs::IO::extension(fn);
304 return fn.substr(0, fn.length() - ext.length());
305}
306
307void Cogs::IO::replaceAll(std::string& str, std::string_view from, std::string_view to) {
308 if (from.empty()) return;
309
310 size_t startPos = 0;
311 while ((startPos = str.find(from, startPos)) != std::string::npos) {
312 str.replace(startPos, from.length(), to);
313 startPos += to.length();
314 }
315}
316
317Cogs::IO::FileTimeType Cogs::IO::getFileTime(std::string_view /*path*/) {
318 return {};
319}
320
321uint64_t Cogs::IO::getFileSize(std::string_view /*path*/) {
322 return 0;
323}
324
325bool Cogs::IO::writeBinaryFile(const std::string& /*fileName*/, const void* /*content*/, size_t /*contentSize*/) {
326 return false;
327}
328
329bool Cogs::IO::createDirectories(std::string_view /*fileName*/) {
330 return false;
331}
332
333Cogs::IO::RecursiveDirIterator Cogs::IO::directoryIterator(std::string_view /*directory*/) {
334 return {};
335}
336
337std::string Cogs::IO::getPath(const DirectoryEntry& entry) {
338 return entry;
339}
Log implementation class.
Definition: LogManager.h:139
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:24
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
FileContentsHints
Definition: FileContents.h:11
STL namespace.
Abstract base class storing data read from a file.
Definition: FileContents.h:20
virtual Memory::MemoryBuffer take()=0
Take ownership of underlying memorybuffer if exists.
const uint8_t * ptr
Start of buffer storing file data. Use.
Definition: FileContents.h:27