aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.gitmodules3
-rw-r--r--README.md7
m---------depends/odhtdb0
-rw-r--r--include/FileUtil.hpp21
-rw-r--r--include/dchat/Cache.hpp102
-rw-r--r--include/dchat/Clock.hpp16
-rw-r--r--include/dchat/Color.hpp24
-rw-r--r--include/dchat/Gif.hpp52
-rw-r--r--include/dchat/StringView.hpp95
-rw-r--r--include/dchat/Vec2.hpp32
-rw-r--r--include/dchat/WebPagePreview.hpp13
-rw-r--r--include/dchat/types.hpp22
-rw-r--r--include/env.hpp59
-rw-r--r--project.conf12
-rw-r--r--src/Cache.cpp434
-rw-r--r--src/Clock.cpp25
-rw-r--r--src/FileUtil.cpp55
-rw-r--r--src/Gif.cpp189
-rw-r--r--tests/main.cpp7
20 files changed, 1174 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0dee329
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# Compiled sibs files
+sibs-build/
+compile_commands.json
+tests/sibs-build/
+tests/compile_commands.json
+.vscode/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..8fd227f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "depends/odhtdb"]
+ path = depends/odhtdb
+ url = https://gitlab.com/DEC05EBA/odhtdb.git
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d9f3e93
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# Dchat core
+Useful classes to handle the things that we want all dchat clients to have, for example:
+```
+Channels
+Asynchronous texture/gif/website preview loading
+Video playback to view content on sites such as youtube (by using mpv)
+```
diff --git a/depends/odhtdb b/depends/odhtdb
new file mode 160000
+Subproject 55bb14a7e0d034b375da73a9c2aae10881e3280
diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp
new file mode 100644
index 0000000..cb4e308
--- /dev/null
+++ b/include/FileUtil.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "dchat/StringView.hpp"
+#include <boost/filesystem/path.hpp>
+#include <stdexcept>
+
+namespace dchat
+{
+ class FileException : public std::runtime_error
+ {
+ public:
+ FileException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ // Throws FileException on error.
+ // Returned value is allocated with malloc and should be free'd by caller.
+ StringView getFileContent(const boost::filesystem::path &filepath);
+
+ // Throws FileException on error
+ void fileReplace(const boost::filesystem::path &filepath, const StringView data);
+}
diff --git a/include/dchat/Cache.hpp b/include/dchat/Cache.hpp
new file mode 100644
index 0000000..debcd5b
--- /dev/null
+++ b/include/dchat/Cache.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "types.hpp"
+#include "WebPagePreview.hpp"
+#include "StringView.hpp"
+#include <boost/filesystem/path.hpp>
+#include <string>
+#include <unordered_map>
+#include <thread>
+#include <mutex>
+#include <vector>
+#include <chrono>
+#include <functional>
+
+namespace TinyProcessLib
+{
+ class Process;
+}
+
+namespace dchat
+{
+ class Gif;
+ class WebPagePreview;
+
+ struct ContentByUrlResult
+ {
+ enum class Type
+ {
+ CACHED,
+ DOWNLOADING,
+ FAILED_DOWNLOAD
+ };
+
+ enum class CachedType
+ {
+ NONE,
+ TEXTURE_FILEPATH,
+ GIF,
+ WEB_PAGE_PREVIEW
+ };
+
+ ContentByUrlResult() : textureFilePath(nullptr), type(Type::DOWNLOADING), cachedType(CachedType::NONE) {}
+ ContentByUrlResult(boost::filesystem::path *_textureFilePath, Type _type) : textureFilePath(_textureFilePath), type(_type), cachedType(CachedType::TEXTURE_FILEPATH) {}
+ ContentByUrlResult(Gif *_gif, Type _type) : gif(_gif), type(_type), cachedType(CachedType::GIF) {}
+ ContentByUrlResult(WebPagePreview *_webPagePreview, Type _type) : webPagePreview(_webPagePreview), type(_type), cachedType(CachedType::WEB_PAGE_PREVIEW) {}
+
+ // @texture is null if @type is DOWNLOADING or FAILED_DOWNLOAD
+ union
+ {
+ boost::filesystem::path *textureFilePath;
+ Gif *gif;
+ WebPagePreview *webPagePreview;
+ };
+
+ Type type;
+ CachedType cachedType;
+ i64 lastAccessed;
+ };
+
+ using LoadBindsCallbackFunc = std::function<void(const std::string &key, const std::string &value)>;
+ // @fileContent contains data allocated with new[], deallocate it with delete[] fileContent.data;
+ // Returned gif should be allocated with @new
+ using CreateGifFunc = std::function<Gif*(StringView fileContent)>;
+
+ class Cache
+ {
+ public:
+ // @createGifFunc can't be nullptr
+ Cache(CreateGifFunc createGifFunc);
+ ~Cache();
+
+ // Creates directory if it doesn't exist (recursively). Throws boost exception on failure
+ static boost::filesystem::path getDchatDir();
+
+ // Creates directory if it doesn't exist (recursively). Throws boost exception on failure
+ static boost::filesystem::path getImagesDir();
+
+ // @callbackFunc can't be nullptr
+ static void loadBindsFromFile(LoadBindsCallbackFunc callbackFunc);
+ static void replaceBindsInFile(const std::unordered_map<std::string, std::string> &binds);
+
+ // Get cached content or download it.
+ // Default download file limit is 12MB
+ // Returns ContentByUrlResult describing texture status.
+ const ContentByUrlResult getContentByUrl(const std::string &url, int downloadLimitBytes = 12582912);
+ private:
+ struct ImageDownloadInfo
+ {
+ TinyProcessLib::Process *process;
+ std::string url;
+ };
+
+ std::thread downloadWaitThread;
+ std::thread checkContentAccessTimeThread;
+ std::vector<ImageDownloadInfo> imageDownloadProcesses;
+ std::vector<ImageDownloadInfo> imageDownloadProcessesQueue;
+ std::mutex imageDownloadMutex;
+ bool alive;
+ std::unordered_map<std::string, ContentByUrlResult> contentUrlCache;
+ CreateGifFunc createGifFunc;
+ };
+}
diff --git a/include/dchat/Clock.hpp b/include/dchat/Clock.hpp
new file mode 100644
index 0000000..97f78a8
--- /dev/null
+++ b/include/dchat/Clock.hpp
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "types.hpp"
+
+namespace dchat
+{
+ class Clock
+ {
+ public:
+ Clock();
+ void restart();
+ i64 getElapsedTimeMillis() const;
+ private:
+ i64 startTime;
+ };
+} \ No newline at end of file
diff --git a/include/dchat/Color.hpp b/include/dchat/Color.hpp
new file mode 100644
index 0000000..2a5d121
--- /dev/null
+++ b/include/dchat/Color.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+namespace dchat
+{
+ class Color
+ {
+ public:
+ Color() : Color(255, 255, 255, 255) {}
+ Color(unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha = 255)
+ {
+ data[0] = red;
+ data[1] = green;
+ data[2] = blue;
+ data[3] = alpha;
+ }
+
+ unsigned char red() const { return data[0]; }
+ unsigned char green() const { return data[1]; }
+ unsigned char blue() const { return data[2]; }
+ unsigned char alpha() const { return data[3]; }
+
+ unsigned char data[4];
+ };
+} \ No newline at end of file
diff --git a/include/dchat/Gif.hpp b/include/dchat/Gif.hpp
new file mode 100644
index 0000000..f97ff2f
--- /dev/null
+++ b/include/dchat/Gif.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "StringView.hpp"
+#include "Vec2.hpp"
+#include "Color.hpp"
+#include "Clock.hpp"
+#include <boost/filesystem/path.hpp>
+#include <stdexcept>
+extern "C"
+{
+#include <libnsgif.h>
+}
+
+namespace dchat
+{
+ class GifLoadException : public std::runtime_error
+ {
+ public:
+ GifLoadException(const std::string &errMsg) : std::runtime_error(errMsg) {}
+ };
+
+ class Gif
+ {
+ public:
+ // Throws GifLoadException on error
+ Gif(const boost::filesystem::path &filepath);
+ Gif(StringView fileContent);
+ virtual ~Gif();
+
+ Vec2u getSize() const;
+ void update();
+
+ static bool isDataGif(const StringView &data);
+ protected:
+ // Return false if texture creation failed
+ virtual bool createTexture(int width, int height) = 0;
+ virtual void setPosition(const Vec2f &position) = 0;
+ virtual Vec2f getPosition() const = 0;
+ virtual void setScale(const Vec2f &scale) = 0;
+ virtual void setColor(Color color) = 0;
+ // Size of texture data is same as the size that the texture was created with (also same size returned by @getSize function)
+ virtual void updateTexture(void *textureData) = 0;
+ private:
+ void init();
+ private:
+ gif_animation gif;
+ StringView fileContent;
+ unsigned int currentFrame;
+ double timeElapsedCs;
+ Clock frameTimer;
+ };
+}
diff --git a/include/dchat/StringView.hpp b/include/dchat/StringView.hpp
new file mode 100644
index 0000000..659610a
--- /dev/null
+++ b/include/dchat/StringView.hpp
@@ -0,0 +1,95 @@
+#pragma once
+
+#include "types.hpp"
+#include <string.h>
+#include <assert.h>
+
+namespace dchat
+{
+ template <typename CharType>
+ class BasicStringView
+ {
+ public:
+ BasicStringView() : data(nullptr), size(0)
+ {
+
+ }
+
+ BasicStringView(const BasicStringView<CharType> &other) : data(other.data), size(other.size)
+ {
+
+ }
+
+ BasicStringView(const CharType *_data) : data(_data), size(strlen(_data))
+ {
+
+ }
+
+ BasicStringView(const CharType *_data, usize _size) : data(_data), size(_size)
+ {
+
+ }
+
+ BasicStringView<CharType>& operator = (const BasicStringView<CharType> &other)
+ {
+ data = other.data;
+ size = other.size;
+ return *this;
+ }
+
+ BasicStringView(BasicStringView<CharType> &&other)
+ {
+ data = other.data;
+ size = other.size;
+
+ other.data = nullptr;
+ other.size = 0;
+ }
+
+ bool equals(const BasicStringView<CharType> &other) const
+ {
+ if(size != other.size) return false;
+ return memcmp(data, other.data, size * sizeof(CharType)) == 0;
+ }
+
+ bool operator == (const BasicStringView<CharType> &other) const
+ {
+ return equals(other);
+ }
+
+ bool operator != (const BasicStringView<CharType> &other) const
+ {
+ return !equals(other);
+ }
+
+ CharType operator [] (usize index) const
+ {
+ assert(index < size);
+ return data[index];
+ }
+
+ // Returns -1 if substr not found.
+ // TODO: Make this more efficient
+ usize find(const BasicStringView<CharType> &substr, usize offset = 0) const
+ {
+ if(substr.size == 0)
+ return -1;
+
+ if(offset + substr.size > size)
+ return -1;
+
+ for(usize i = offset; i < size - (substr.size - 1); ++i)
+ {
+ if(memcmp(data + i, substr.data, substr.size * sizeof(CharType)) == 0)
+ return i;
+ }
+ return -1;
+ }
+
+ const CharType *data;
+ usize size;
+ };
+
+ using StringView = BasicStringView<char>;
+ using StringViewUtf32 = BasicStringView<u32>;
+}
diff --git a/include/dchat/Vec2.hpp b/include/dchat/Vec2.hpp
new file mode 100644
index 0000000..cb5f8c0
--- /dev/null
+++ b/include/dchat/Vec2.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+namespace dchat
+{
+ template <typename T>
+ struct Vec2
+ {
+ T x, y;
+
+ Vec2() : x(), y() {}
+ Vec2(T _x, T _y) : x(_x), y(_y) {}
+/*
+ Vec2(const Vec2<T> &other)
+ {
+ x = other.x;
+ y = other.y;
+ }
+
+ Vec2<T>& operator = (const Vec2<T> &other)
+ {
+ x = other.x;
+ y = other.y;
+ return *this;
+ }
+*/
+ };
+
+ using Vec2f = Vec2<float>;
+ using Vec2d = Vec2<double>;
+ using Vec2i = Vec2<int>;
+ using Vec2u = Vec2<unsigned int>;
+} \ No newline at end of file
diff --git a/include/dchat/WebPagePreview.hpp b/include/dchat/WebPagePreview.hpp
new file mode 100644
index 0000000..df75419
--- /dev/null
+++ b/include/dchat/WebPagePreview.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <string>
+
+namespace dchat
+{
+ class WebPagePreview
+ {
+ public:
+ std::string title;
+ std::string description;
+ };
+}
diff --git a/include/dchat/types.hpp b/include/dchat/types.hpp
new file mode 100644
index 0000000..97bfc96
--- /dev/null
+++ b/include/dchat/types.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <stdint.h>
+
+namespace dchat
+{
+ typedef int8_t i8;
+ typedef int16_t i16;
+ typedef int32_t i32;
+ typedef int64_t i64;
+
+ typedef uint8_t u8;
+ typedef uint16_t u16;
+ typedef uint32_t u32;
+ typedef uint64_t u64;
+
+ typedef float f32;
+ typedef double f64;
+
+ typedef intptr_t ssize;
+ typedef uintptr_t usize;
+}
diff --git a/include/env.hpp b/include/env.hpp
new file mode 100644
index 0000000..4061518
--- /dev/null
+++ b/include/env.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#define OS_FAMILY_WINDOWS 0
+#define OS_FAMILY_POSIX 1
+
+#define OS_TYPE_WINDOWS 0
+#define OS_TYPE_LINUX 1
+
+#if defined(_WIN32) || defined(_WIN64)
+ #if defined(_WIN64)
+ #define SYS_ENV_64BIT
+ #else
+ #define SYS_ENV_32BIT
+ #endif
+ #define OS_FAMILY OS_FAMILY_WINDOWS
+ #define OS_TYPE OS_TYPE_WINDOWS
+
+ #ifndef UNICODE
+ #define UNICODE
+ #endif
+
+ #ifndef _UNICODE
+ #define _UNICODE
+ #endif
+
+ #ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+ #endif
+
+ #include <Windows.h>
+#endif
+
+#if defined(__linux__) || defined(__unix__) || defined(__APPLE__) || defined(_POSIX_VERSION)
+ #define OS_FAMILY OS_FAMILY_POSIX
+#endif
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define OS_TYPE OS_TYPE_LINUX
+#endif
+
+#if defined(__GNUC__)
+ #if defined(__x86_64__) || defined(__pc64__)
+ #define SYS_ENV_64BIT
+ #else
+ #define SYS_ENV_32BIT
+ #endif
+#endif
+
+#if !defined(SYS_ENV_32BIT) && !defined(SYS_ENV_64BIT)
+ #error "System is not detected as either 32-bit or 64-bit"
+#endif
+
+#if !defined(OS_FAMILY)
+ #error "System not supported. Only Windows and Posix systems supported right now"
+#endif
+
+#if !defined(OS_TYPE)
+ #error "System not supported. Only Windows and linux systems supported right now"
+#endif
diff --git a/project.conf b/project.conf
new file mode 100644
index 0000000..7bfa3b1
--- /dev/null
+++ b/project.conf
@@ -0,0 +1,12 @@
+[package]
+name = "dchat_core"
+type = "dynamic"
+version = "0.1.0"
+platforms = ["any"]
+
+[dependencies]
+tiny-process = "2"
+boost-filesystem = "1.66"
+libpreview = ">=0.2.0"
+libgd = "2"
+libnsgif = "0.2.0" \ No newline at end of file
diff --git a/src/Cache.cpp b/src/Cache.cpp
new file mode 100644
index 0000000..0bde89d
--- /dev/null
+++ b/src/Cache.cpp
@@ -0,0 +1,434 @@
+#include "../include/dchat/Cache.hpp"
+#include "../include/env.hpp"
+#include "../include/FileUtil.hpp"
+#include "../include/dchat/Gif.hpp"
+#include <boost/filesystem/convenience.hpp>
+#include <process.hpp>
+#include <odhtdb/Hash.hpp>
+#include <sibs/SafeSerializer.hpp>
+#include <sibs/SafeDeserializer.hpp>
+#include <libpreview.h>
+#include <gd.h>
+
+#if OS_FAMILY == OS_FAMILY_POSIX
+#include <pwd.h>
+#else
+#include <string>
+#endif
+
+using namespace std;
+using namespace TinyProcessLib;
+
+namespace dchat
+{
+ const i64 CONTENT_NOT_VISIBLE_AGE_MS = 30000; // Delete content from cache after a specified amount of time if the content is not visible on the screen
+
+ static boost::filesystem::path getHomeDir()
+ {
+ #if OS_FAMILY == OS_FAMILY_POSIX
+ const char *homeDir = getenv("HOME");
+ if(!homeDir)
+ {
+ passwd *pw = getpwuid(getuid());
+ homeDir = pw->pw_dir;
+ }
+ return boost::filesystem::path(homeDir);
+ #elif OS_FAMILY == OS_FAMILY_WINDOWS
+ BOOL ret;
+ HANDLE hToken;
+ std::wstring homeDir;
+ DWORD homeDirLen = MAX_PATH;
+ homeDir.resize(homeDirLen);
+
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken))
+ return Result<FileString>::Err("Failed to open process token");
+
+ if (!GetUserProfileDirectory(hToken, &homeDir[0], &homeDirLen))
+ {
+ CloseHandle(hToken);
+ return Result<FileString>::Err("Failed to get home directory");
+ }
+
+ CloseHandle(hToken);
+ homeDir.resize(wcslen(homeDir.c_str()));
+ return boost::filesystem::path(homeDir);
+ #endif
+ }
+
+ boost::filesystem::path Cache::getDchatDir()
+ {
+ boost::filesystem::path dchatHomeDir = getHomeDir() / ".local" / "share" / "dchat";
+ boost::filesystem::create_directories(dchatHomeDir);
+ return dchatHomeDir;
+ }
+
+ boost::filesystem::path Cache::getImagesDir()
+ {
+ boost::filesystem::path imagesDir = getDchatDir() / "images";
+ boost::filesystem::create_directories(imagesDir);
+ return imagesDir;
+ }
+
+ void Cache::loadBindsFromFile(LoadBindsCallbackFunc callbackFunc)
+ {
+ assert(callbackFunc);
+ StringView fileContent;
+ try
+ {
+ fileContent = getFileContent(getDchatDir() / "binds");
+ sibs::SafeDeserializer deserializer((const u8*)fileContent.data, fileContent.size);
+
+ while(!deserializer.empty())
+ {
+ u8 keySize = deserializer.extract<u8>();
+ string key;
+ key.resize(keySize);
+ deserializer.extract((u8*)&key[0], keySize);
+
+ u8 valueSize = deserializer.extract<u8>();
+ string value;
+ value.resize(valueSize);
+ deserializer.extract((u8*)&value[0], valueSize);
+
+ callbackFunc(key, value);
+ }
+ }
+ catch(FileException &e)
+ {
+ fprintf(stderr, "Failed to read binds from file, reason: %s\n", e.what());
+ }
+
+ delete[] fileContent.data;
+ }
+
+ void Cache::replaceBindsInFile(const unordered_map<string, string> &binds)
+ {
+ sibs::SafeSerializer serializer;
+ for(auto &it : binds)
+ {
+ serializer.add((u8)it.first.size());
+ serializer.add((const u8*)it.first.data(), it.first.size());
+
+ serializer.add((u8)it.second.size());
+ serializer.add((const u8*)it.second.data(), it.second.size());
+ }
+ fileReplace(getDchatDir() / "binds", StringView((const char*)serializer.getBuffer().data(), serializer.getBuffer().size()));
+ }
+
+ static bool downscaleImage(const boost::filesystem::path &filepath, void *data, const int size, const int newWidth, const int newHeight)
+ {
+ gdImagePtr imgPtr = gdImageCreateFromPngPtr(size, data);
+ if(!imgPtr)
+ return false;
+
+ int width = gdImageSX(imgPtr);
+ if(width < newWidth)
+ {
+ gdImageDestroy(imgPtr);
+ return false;
+ }
+
+ int height = gdImageSX(imgPtr);
+ if(height < newHeight)
+ {
+ gdImageDestroy(imgPtr);
+ return false;
+ }
+
+ gdImageSetInterpolationMethod(imgPtr, GD_BILINEAR_FIXED);
+ gdImagePtr newImgPtr = gdImageScale(imgPtr, newWidth, newHeight);
+ if(!newImgPtr)
+ {
+ gdImageDestroy(imgPtr);
+ return false;
+ }
+
+ bool success = (gdImageFile(newImgPtr, filepath.c_str()) == 0);
+ gdImageDestroy(imgPtr);
+ gdImageDestroy(newImgPtr);
+ return success;
+ }
+
+ static ContentByUrlResult loadImageFromFile(const boost::filesystem::path &filepath, bool loadFromCache, CreateGifFunc createGifFunc)
+ {
+ StringView fileContent;
+ try
+ {
+ fileContent = getFileContent(filepath);
+
+ std::string webPageTitle;
+ std::string webPageDescription;
+ bool foundHtmlContent = false;
+ preview_state state;
+ preview_init(&state);
+ size_t offset = 0;
+ do
+ {
+ // TODO: Get file content before doing this, the file might be in utf-16 encoding. That can happen for example if file contains html.
+ // Content type can be retrieved from HTTP response header when downloading content
+ offset += preview_step(&state, fileContent.data + offset, fileContent.size - offset);
+ if(state.step_result == PREVIEW_FOUND_IMAGE)
+ {
+ if(Gif::isDataGif(fileContent))
+ {
+ Gif *gif = createGifFunc(fileContent);
+ return { gif, ContentByUrlResult::Type::CACHED };
+ }
+ else
+ {
+ if(!loadFromCache)
+ {
+ if(!downscaleImage(filepath, (void*)fileContent.data, fileContent.size, 100, 100))
+ {
+ fprintf(stderr, "Failed to resize image: %s, using original file\n", filepath.c_str());
+ }
+ }
+
+ delete[] fileContent.data;
+ fileContent.data = nullptr;
+ return { new boost::filesystem::path(filepath), ContentByUrlResult::Type::CACHED };
+ }
+ break;
+ }
+ else if(state.step_result == PREVIEW_FOUND_TITLE)
+ {
+ foundHtmlContent = true;
+ webPageTitle = std::string(state.meta_content, state.meta_content + state.meta_content_length);
+ }
+ else if(state.step_result == PREVIEW_FOUND_DESCRIPTION)
+ {
+ foundHtmlContent = true;
+ webPageDescription = std::string(state.meta_content, state.meta_content + state.meta_content_length);
+ }
+ } while(offset < fileContent.size);
+
+ delete[] fileContent.data;
+ fileContent.data = nullptr;
+
+ if(foundHtmlContent)
+ {
+ WebPagePreview *webPagePreview = new WebPagePreview { webPageTitle, webPageDescription };
+ return { webPagePreview, ContentByUrlResult::Type::CACHED };
+ }
+ }
+ catch(std::exception &e)
+ {
+ fprintf(stderr, "Failed to load image %s, reason: %s\n", filepath.string().c_str(), e.what());
+ }
+ return { (boost::filesystem::path*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD };
+ }
+
+ Cache::Cache(CreateGifFunc _createGifFunc) :
+ alive(true),
+ createGifFunc(_createGifFunc)
+ {
+ assert(createGifFunc);
+ downloadWaitThread = thread([this]
+ {
+ while(alive)
+ {
+ for(vector<ImageDownloadInfo>::iterator it = imageDownloadProcesses.begin(); it != imageDownloadProcesses.end();)
+ {
+ int exitStatus;
+ if(it->process->try_get_exit_status(exitStatus))
+ {
+ boost::filesystem::path filepath = getImagesDir();
+ odhtdb::Hash urlHash(it->url.data(), it->url.size());
+ filepath /= urlHash.toString();
+
+ ContentByUrlResult contentByUrlResult;
+ bool failed = exitStatus != 0;
+ if(failed)
+ {
+ contentByUrlResult = { (boost::filesystem::path*)nullptr, ContentByUrlResult::Type::FAILED_DOWNLOAD };
+ }
+ else
+ {
+ contentByUrlResult = loadImageFromFile(filepath, false, createGifFunc);
+ contentByUrlResult.lastAccessed = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
+ if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED)
+ {
+ printf("Download content from url: %s\n", it->url.c_str());
+ }
+ }
+
+ imageDownloadMutex.lock();
+ contentUrlCache[it->url] = contentByUrlResult;
+ boost::filesystem::path downloadingFilepath = filepath;
+ downloadingFilepath += ".downloading";
+ // Intentionally ignore failure, program should not crash if we fail to remove these files...
+ boost::system::error_code err;
+ boost::filesystem::remove(downloadingFilepath, err);
+ imageDownloadMutex.unlock();
+ it = imageDownloadProcesses.erase(it);
+ }
+ else
+ ++it;
+ }
+
+ while(alive && imageDownloadProcesses.empty() && imageDownloadProcessesQueue.empty())
+ this_thread::sleep_for(chrono::milliseconds(20));
+
+ if(!imageDownloadProcessesQueue.empty())
+ {
+ imageDownloadMutex.lock();
+ for(auto imageDownloadInfo : imageDownloadProcessesQueue)
+ {
+ imageDownloadProcesses.push_back(imageDownloadInfo);
+ }
+ imageDownloadProcessesQueue.clear();
+ imageDownloadMutex.unlock();
+ }
+
+ this_thread::sleep_for(chrono::milliseconds(20));
+ }
+ });
+
+ checkContentAccessTimeThread = thread([this]
+ {
+ while(alive)
+ {
+ this_thread::sleep_for(chrono::milliseconds(500));
+ lock_guard<mutex> lock(imageDownloadMutex);
+
+ i64 now = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
+ for(unordered_map<string, ContentByUrlResult>::iterator it = contentUrlCache.begin(); it != contentUrlCache.end();)
+ {
+ if(it->second.type == ContentByUrlResult::Type::CACHED && now - it->second.lastAccessed > CONTENT_NOT_VISIBLE_AGE_MS)
+ {
+ switch(it->second.cachedType)
+ {
+ case ContentByUrlResult::CachedType::TEXTURE_FILEPATH:
+ {
+ delete it->second.textureFilePath;
+ break;
+ }
+ case ContentByUrlResult::CachedType::GIF:
+ {
+ delete it->second.gif;
+ break;
+ }
+ case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW:
+ {
+ delete it->second.webPagePreview;
+ break;
+ }
+ default:
+ ++it;
+ continue;
+ }
+ it = contentUrlCache.erase(it);
+ }
+ else
+ ++it;
+ }
+ }
+ });
+ }
+
+ Cache::~Cache()
+ {
+ alive = false;
+ downloadWaitThread.join();
+ checkContentAccessTimeThread.join();
+
+ for(auto &it : contentUrlCache)
+ {
+ if(!it.second.textureFilePath)
+ continue;
+
+ switch(it.second.cachedType)
+ {
+ case ContentByUrlResult::CachedType::NONE:
+ break;
+ case ContentByUrlResult::CachedType::TEXTURE_FILEPATH:
+ {
+ delete it.second.textureFilePath;
+ break;
+ }
+ case ContentByUrlResult::CachedType::GIF:
+ {
+ delete it.second.gif;
+ break;
+ }
+ case ContentByUrlResult::CachedType::WEB_PAGE_PREVIEW:
+ {
+ delete it.second.webPagePreview;
+ break;
+ }
+ default:
+ // Did we forget to implement cleanup for a new cache content type?
+ assert(false);
+ break;
+ }
+ }
+ }
+
+ static void createFileIgnoreError(const boost::filesystem::path &path)
+ {
+ try
+ {
+ fileReplace(path, StringView("", 0));
+ }
+ catch(FileException &e)
+ {
+ fprintf(stderr, "Failed to create empty file: %s, reason: %s\n", path.string().c_str(), e.what());
+ }
+ }
+
+ const ContentByUrlResult Cache::getContentByUrl(const string &url, int downloadLimitBytes)
+ {
+ lock_guard<mutex> lock(imageDownloadMutex);
+ auto it = contentUrlCache.find(url);
+ if(it != contentUrlCache.end())
+ {
+ it->second.lastAccessed = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
+ return it->second;
+ }
+
+ // TODO: Verify hashed url is not too long for filepath on windows
+ boost::filesystem::path filepath = getImagesDir();
+ odhtdb::Hash urlHash(url.data(), url.size());
+ filepath /= urlHash.toString();
+
+ boost::filesystem::path downloadingFilepath = filepath;
+ downloadingFilepath += ".downloading";
+ if(boost::filesystem::exists(downloadingFilepath))
+ {
+ // Intentionally ignore failure, program should not crash if we fail to remove these files...
+ boost::system::error_code err;
+ boost::filesystem::remove(filepath, err);
+ boost::filesystem::remove(downloadingFilepath, err);
+ }
+
+ // TODO: Do not load content in this thread. Return LOADING status and load it in another thread, because with a lot of images, chat can freeze
+ ContentByUrlResult contentByUrlResult = loadImageFromFile(filepath, true, createGifFunc);
+ if(contentByUrlResult.type == ContentByUrlResult::Type::CACHED)
+ {
+ contentByUrlResult.lastAccessed = chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
+ contentUrlCache[url] = contentByUrlResult;
+ printf("Loaded content from file cache: %s\n", url.c_str());
+ return contentByUrlResult;
+ }
+ else if(contentByUrlResult.type == ContentByUrlResult::Type::FAILED_DOWNLOAD && boost::filesystem::exists(filepath))
+ {
+ contentUrlCache[url] = contentByUrlResult;
+ return contentByUrlResult;
+ }
+
+ createFileIgnoreError(downloadingFilepath);
+ ContentByUrlResult result((boost::filesystem::path*)nullptr, ContentByUrlResult::Type::DOWNLOADING);
+ contentUrlCache[url] = result;
+
+ string downloadLimitBytesStr = to_string(downloadLimitBytes);
+
+ Process::string_type cmd = "curl -L --silent -o '";
+ cmd += filepath.native();
+ cmd += "' --max-filesize " + downloadLimitBytesStr + " --range 0-" + downloadLimitBytesStr + " --url '" + url + "'";
+ // TODO: Use this instead of curl on windows: certutil.exe -urlcache -split -f "https://url/to/file" path/and/name/to/save/as/file
+ Process *process = new Process(cmd, "", nullptr, nullptr, false);
+ ImageDownloadInfo imageDownloadInfo { process, url };
+ imageDownloadProcessesQueue.emplace_back(imageDownloadInfo);
+ return result;
+ }
+}
diff --git a/src/Clock.cpp b/src/Clock.cpp
new file mode 100644
index 0000000..1dc9323
--- /dev/null
+++ b/src/Clock.cpp
@@ -0,0 +1,25 @@
+#include "../include/dchat/Clock.hpp"
+#include <chrono>
+
+namespace dchat
+{
+ static i64 getCurrentTimeMillis()
+ {
+ return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
+ }
+
+ Clock::Clock()
+ {
+ restart();
+ }
+
+ void Clock::restart()
+ {
+ startTime = getCurrentTimeMillis();
+ }
+
+ i64 Clock::getElapsedTimeMillis() const
+ {
+ return getCurrentTimeMillis() - startTime;
+ }
+} \ No newline at end of file
diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp
new file mode 100644
index 0000000..08efd00
--- /dev/null
+++ b/src/FileUtil.cpp
@@ -0,0 +1,55 @@
+#include "../include/FileUtil.hpp"
+#include "../include/env.hpp"
+#include <stdio.h>
+
+namespace dchat
+{
+ StringView getFileContent(const boost::filesystem::path &filepath)
+ {
+#if OS_FAMILY == OS_FAMILY_POSIX
+ FILE *file = fopen(filepath.c_str(), "rb");
+#else
+ FILE *file = _wfopen(filepath.c_str(), L"rb");
+#endif
+ if(!file)
+ {
+ int error = errno;
+ std::string errMsg = "Failed to open file: ";
+ errMsg += filepath.string();
+ errMsg += "; reason: ";
+ errMsg += strerror(error);
+ throw FileException(errMsg);
+ }
+
+ fseek(file, 0, SEEK_END);
+ size_t fileSize = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ char *fileData = new char[fileSize];
+ fread(fileData, 1, fileSize, file);
+ fclose(file);
+ return { fileData, fileSize };
+ }
+
+ void fileReplace(const boost::filesystem::path &filepath, const StringView data)
+ {
+#if OS_FAMILY == OS_FAMILY_POSIX
+ FILE *file = fopen(filepath.string().c_str(), "wb+");
+#else
+ FILE *file = _wfopen(filepath.wstring().c_str(), L"wb+");
+#endif
+ if(!file)
+ {
+ int error = errno;
+ std::string errMsg = "Failed to replace file: ";
+ errMsg += filepath.string();
+ errMsg += ", reason: ";
+ errMsg += strerror(error);
+ throw FileException(errMsg);
+ }
+
+ setbuf(file, NULL);
+ fwrite(data.data, 1, data.size, file);
+ fclose(file);
+ }
+}
diff --git a/src/Gif.cpp b/src/Gif.cpp
new file mode 100644
index 0000000..0c52f7d
--- /dev/null
+++ b/src/Gif.cpp
@@ -0,0 +1,189 @@
+#include "../include/dchat/Gif.hpp"
+#include "../include/FileUtil.hpp"
+
+using namespace std;
+
+namespace dchat
+{
+ static void* bitmapCreate(int width, int height)
+ {
+ return calloc(width * height, 4);
+ }
+
+ static void bitmapDestroy(void *bitmap)
+ {
+ free(bitmap);
+ }
+
+ static unsigned char* bitmapGetBuffer(void *bitmap)
+ {
+ return (unsigned char*)bitmap;
+ }
+
+ static void bitmapSetOpaque(void *bitmap, bool opaque)
+ {
+
+ }
+
+ static bool bitmapTestOpaque(void *bitmap)
+ {
+ return false;
+ }
+
+ static void bitmapModified(void *bitmap)
+ {
+
+ }
+
+ static const char* gifResultToString(gif_result code)
+ {
+ switch(code)
+ {
+ case GIF_INSUFFICIENT_FRAME_DATA:
+ return "GIF_INSUFFICIENT_FRAME_DATA";
+ case GIF_FRAME_DATA_ERROR:
+ return "GIF_FRAME_DATA_ERROR";
+ case GIF_INSUFFICIENT_DATA:
+ return "GIF_INSUFFICIENT_DATA";
+ case GIF_DATA_ERROR:
+ return "GIF_DATA_ERROR";
+ case GIF_INSUFFICIENT_MEMORY:
+ return "GIF_INSUFFICIENT_MEMORY";
+ default:
+ return "Unknown gif result code";
+ }
+ }
+
+ Gif::Gif(const boost::filesystem::path &filepath) :
+ currentFrame(0),
+ timeElapsedCs(0.0)
+ {
+ try
+ {
+ fileContent = getFileContent(filepath);
+ }
+ catch(FileException &e)
+ {
+ throw GifLoadException(e.what());
+ }
+
+ try
+ {
+ init();
+ }
+ catch(GifLoadException &e)
+ {
+ delete[] fileContent.data;
+ throw e;
+ }
+ }
+
+ Gif::Gif(StringView _fileContent) :
+ fileContent(move(_fileContent)),
+ currentFrame(0),
+ timeElapsedCs(0.0)
+ {
+ try
+ {
+ init();
+ }
+ catch(GifLoadException &e)
+ {
+ delete[] fileContent.data;
+ throw e;
+ }
+ }
+
+ void Gif::init()
+ {
+ gif_bitmap_callback_vt bitmapCallbacks =
+ {
+ bitmapCreate,
+ bitmapDestroy,
+ bitmapGetBuffer,
+ bitmapSetOpaque,
+ bitmapTestOpaque,
+ bitmapModified
+ };
+
+ gif_create(&gif, &bitmapCallbacks);
+
+ gif_result code;
+ do
+ {
+ code = gif_initialise(&gif, fileContent.size, (unsigned char*)fileContent.data);
+ if(code != GIF_OK && code != GIF_WORKING)
+ {
+ string errMsg = "Failed to initialize gif, reason: ";
+ errMsg += gifResultToString(code);
+ throw GifLoadException(errMsg);
+ }
+ }
+ while(code != GIF_OK);
+
+ if(!createTexture(gif.width, gif.height))
+ throw GifLoadException("Failed to create texture for gif");
+ }
+
+ Gif::~Gif()
+ {
+ gif_finalise(&gif);
+ delete[] fileContent.data;
+ }
+
+ Vec2u Gif::getSize() const
+ {
+ return { gif.width, gif.height };
+ }
+
+ void Gif::update()
+ {
+ double timeElapsedMilli = (double)frameTimer.getElapsedTimeMillis();
+ // If gif is not redrawn for a while, then we reset timer (gif is paused). This happens when gif is not visible and then appears visible
+ // (because it's visible in window). The reason this is done is to prevent too much time between rendering gif frames, as processing a gif
+ // requires to process all frames between two points in time, if elapsed frame time is too high, then we would require to process several
+ // frames of gif in one application render frame.
+ if(timeElapsedMilli > 1000.0)
+ timeElapsedMilli = 0.0;
+ double frameDeltaCs = timeElapsedMilli * 0.1; // Centisecond
+ frameTimer.restart();
+ timeElapsedCs += frameDeltaCs;
+
+ unsigned char *image = nullptr;
+ u32 startFrame = currentFrame;
+ while(true)
+ {
+ u32 i = currentFrame % gif.frame_count;
+ gif_result code = gif_decode_frame(&gif, i);
+ if(code != GIF_OK)
+ {
+ printf("Warning: gif_decode_frame: %s\n", gifResultToString(code));
+ break;
+ }
+
+ gif_frame &frame = gif.frames[i];
+ // frame_delay is in centiseconds
+ unsigned int frameDelay = frame.frame_delay;
+ if(frameDelay == 0)
+ frameDelay = 7;
+ double fFrameDelay = (double)frameDelay;
+ if(timeElapsedCs >= fFrameDelay)
+ timeElapsedCs -= fFrameDelay;
+ else
+ break;
+
+ image = (unsigned char*)gif.frame_image;
+ ++currentFrame;
+ }
+
+ if(currentFrame != startFrame)
+ {
+ updateTexture(image);
+ }
+ }
+
+ bool Gif::isDataGif(const StringView &data)
+ {
+ return data.size >= 6 && (memcmp(data.data, "GIF87a", 6) == 0 || memcmp(data.data, "GIF89a", 6) == 0);
+ }
+}
diff --git a/tests/main.cpp b/tests/main.cpp
new file mode 100644
index 0000000..9ad80a6
--- /dev/null
+++ b/tests/main.cpp
@@ -0,0 +1,7 @@
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+ printf("hello, world!\n");
+ return 0;
+}