aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Cache.cpp434
-rw-r--r--src/Clock.cpp25
-rw-r--r--src/FileUtil.cpp55
-rw-r--r--src/Gif.cpp189
4 files changed, 703 insertions, 0 deletions
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);
+ }
+}