=> 75197707e0bb149cb9c2e4a983d92fdfb381c17f
[1mdiff --git a/CMakeLists.txt b/CMakeLists.txt[m [1mindex bcf1c7bf..69ac3df0 100644[m [1m--- a/CMakeLists.txt[m [1m+++ b/CMakeLists.txt[m [36m@@ -150,6 +150,8 @@[m [mset (SOURCES[m src/bookmarks.c[m src/bookmarks.h[m src/defs.h[m [32m+[m[32m src/export.c[m [32m+[m[32m src/export.h[m src/feeds.c[m src/feeds.h[m src/fontpack.c[m [1mdiff --git a/lib/the_Foundation b/lib/the_Foundation[m [1mindex 05fe3fdb..32e91939 160000[m [1m--- a/lib/the_Foundation[m [1m+++ b/lib/the_Foundation[m [36m@@ -1 +1 @@[m [31m-Subproject commit 05fe3fdb17ebf64f1830dc7bbe3359e5f062b1f0[m [32m+[m[32mSubproject commit 32e91939443d6e831eb438a81b6a8bc5e17ac021[m [1mdiff --git a/po/en.po b/po/en.po[m [1mindex 699fabb9..f8cc0caa 100644[m [1m--- a/po/en.po[m [1m+++ b/po/en.po[m [36m@@ -9,6 +9,9 @@[m [mmsgstr "Loading"[m msgid "doc.archive"[m msgstr "%s is a compressed archive."[m [m [32m+[m[32mmsgid "heading.archive.userdata"[m [32m+[m[32mmsgstr "User Data Archive"[m [32m+[m msgid "doc.archive.view"[m msgstr "View archive contents"[m [m [36m@@ -181,6 +184,12 @@[m [mmsgstr "Open Location…"[m msgid "menu.downloads"[m msgstr "Show Downloads"[m [m [32m+[m[32mmsgid "menu.export"[m [32m+[m[32mmsgstr "Export User Data"[m [32m+[m [32m+[m[32mmsgid "menu.import"[m [32m+[m[32mmsgstr "Import User Data…"[m [32m+[m msgid "menu.pageinfo"[m msgstr "Show Page Information"[m [m [36m@@ -971,6 +980,9 @@[m [mmsgstr "Import Certificate/Key File"[m msgid "dlg.certimport.import"[m msgstr "Import"[m [m [32m+[m[32mmsgid "dlg.certimport.paste"[m [32m+[m[32mmsgstr "Paste from Clipboard"[m [32m+[m msgid "dlg.certimport.notes"[m msgstr "Notes:"[m [m [36m@@ -989,6 +1001,48 @@[m [mmsgstr "Audio"[m msgid "link.hint.image"[m msgstr "Image"[m [m [32m+[m[32mmsgid "heading.import.userdata"[m [32m+[m[32mmsgstr "Import User Data"[m [32m+[m [32m+[m[32mmsgid "heading.import.userdata.error"[m [32m+[m[32mmsgstr "Import Failed"[m [32m+[m [32m+[m[32mmsgid "import.userdata.bookmarks"[m [32m+[m[32mmsgstr "Bookmarks:"[m [32m+[m [32m+[m[32mmsgid "import.userdata.idents"[m [32m+[m[32mmsgstr "Identities:"[m [32m+[m [32m+[m[32mmsgid "import.userdata.history"[m [32m+[m[32mmsgstr "History:"[m [32m+[m [32m+[m[32mmsgid "import.userdata.trusted"[m [32m+[m[32mmsgstr "Trusted certificates:"[m [32m+[m [32m+[m[32mmsgid "import.userdata.sitespec"[m [32m+[m[32mmsgstr "Site settings:"[m [32m+[m [32m+[m[32mmsgid "dlg.userdata.no"[m [32m+[m[32mmsgstr "None"[m [32m+[m [32m+[m[32mmsgid "dlg.userdata.missing"[m [32m+[m[32mmsgstr "If Missing"[m [32m+[m [32m+[m[32mmsgid "dlg.userdata.all"[m [32m+[m[32mmsgstr "All"[m [32m+[m [32m+[m[32mmsgid "dlg.userdata.alldup"[m [32m+[m[32mmsgstr "All (Keep Duplicates)"[m [32m+[m [32m+[m[32mmsgid "import.userdata"[m [32m+[m[32mmsgstr "Import Selected Data"[m [32m+[m [32m+[m[32mmsgid "import.userdata.dupfolder"[m [32m+[m[32mmsgstr "Imported Duplicates"[m [32m+[m [32m+[m[32mmsgid "import.userdata.error"[m [32m+[m[32mmsgstr "%s is not a valid Lagrange export archive."[m [32m+[m msgid "bookmark.title.blank"[m msgstr "Blank Page"[m [m [1mdiff --git a/res/lang/cs.bin b/res/lang/cs.bin[m [1mindex e70b4c84..d2889c1a 100644[m Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ [1mdiff --git a/res/lang/de.bin b/res/lang/de.bin[m [1mindex a96ffb17..cc4b305e 100644[m Binary files a/res/lang/de.bin and b/res/lang/de.bin differ [1mdiff --git a/res/lang/en.bin b/res/lang/en.bin[m [1mindex c7ce6d11..b1801eec 100644[m Binary files a/res/lang/en.bin and b/res/lang/en.bin differ [1mdiff --git a/res/lang/eo.bin b/res/lang/eo.bin[m [1mindex bdb009e9..155ef35c 100644[m Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ [1mdiff --git a/res/lang/es.bin b/res/lang/es.bin[m [1mindex f20d84da..3f8b7a61 100644[m Binary files a/res/lang/es.bin and b/res/lang/es.bin differ [1mdiff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin[m [1mindex 8770ba16..b19bfa09 100644[m Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ [1mdiff --git a/res/lang/fi.bin b/res/lang/fi.bin[m [1mindex 28f7f22e..b7c7acb8 100644[m Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ [1mdiff --git a/res/lang/fr.bin b/res/lang/fr.bin[m [1mindex 97a48bb2..28eb98ae 100644[m Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ [1mdiff --git a/res/lang/gl.bin b/res/lang/gl.bin[m [1mindex 274bcf2b..e79c0db3 100644[m Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ [1mdiff --git a/res/lang/hu.bin b/res/lang/hu.bin[m [1mindex b2bbc538..c72f74ad 100644[m Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ [1mdiff --git a/res/lang/ia.bin b/res/lang/ia.bin[m [1mindex 9aa98b70..0527016f 100644[m Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ [1mdiff --git a/res/lang/ie.bin b/res/lang/ie.bin[m [1mindex 317b9ddb..85cfd1a9 100644[m Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ [1mdiff --git a/res/lang/isv.bin b/res/lang/isv.bin[m [1mindex 343d17e4..8e7c8169 100644[m Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ [1mdiff --git a/res/lang/it.bin b/res/lang/it.bin[m [1mindex ef145008..e1296993 100644[m Binary files a/res/lang/it.bin and b/res/lang/it.bin differ [1mdiff --git a/res/lang/nl.bin b/res/lang/nl.bin[m [1mindex 7bec0514..85470de6 100644[m Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ [1mdiff --git a/res/lang/pl.bin b/res/lang/pl.bin[m [1mindex 171356c7..ec8e61da 100644[m Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ [1mdiff --git a/res/lang/ru.bin b/res/lang/ru.bin[m [1mindex 92169af5..1467d74c 100644[m Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ [1mdiff --git a/res/lang/sk.bin b/res/lang/sk.bin[m [1mindex 4d197933..b92fdc80 100644[m Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ [1mdiff --git a/res/lang/sr.bin b/res/lang/sr.bin[m [1mindex d733cbef..75e3ac79 100644[m Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ [1mdiff --git a/res/lang/tok.bin b/res/lang/tok.bin[m [1mindex 6ac6ac9f..f501f91c 100644[m Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ [1mdiff --git a/res/lang/tr.bin b/res/lang/tr.bin[m [1mindex 0dc81e68..7ac94fc4 100644[m Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ [1mdiff --git a/res/lang/uk.bin b/res/lang/uk.bin[m [1mindex 4ada5dfe..82e8208a 100644[m Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ [1mdiff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin[m [1mindex 818445b6..4697c37e 100644[m Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ [1mdiff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin[m [1mindex 17d95f10..54050a83 100644[m Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ [1mdiff --git a/src/app.c b/src/app.c[m [1mindex e2802644..e317e4c6 100644[m [1m--- a/src/app.c[m [1m+++ b/src/app.c[m [36m@@ -23,17 +23,17 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include "app.h"[m #include "bookmarks.h"[m #include "defs.h"[m [31m-#include "resources.h"[m [32m+[m[32m#include "export.h"[m #include "feeds.h"[m [31m-#include "mimehooks.h"[m #include "gmcerts.h"[m #include "gmdocument.h"[m #include "gmutil.h"[m #include "history.h"[m #include "ipc.h"[m [32m+[m[32m#include "mimehooks.h"[m #include "periodic.h"[m [32m+[m[32m#include "resources.h"[m #include "sitespec.h"[m [31m-#include "updater.h"[m #include "ui/certimportwidget.h"[m #include "ui/color.h"[m #include "ui/command.h"[m [36m@@ -43,13 +43,15 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include "ui/labelwidget.h"[m #include "ui/root.h"[m #include "ui/sidebarwidget.h"[m [31m-#include "ui/touch.h"[m #include "ui/text.h"[m [32m+[m[32m#include "ui/touch.h"[m #include "ui/uploadwidget.h"[m #include "ui/util.h"[m #include "ui/window.h"[m [32m+[m[32m#include "updater.h"[m #include "visited.h"[m [m [32m+[m[32m#include[m #include [m #include [m #include [m [36m@@ -3744,6 +3746,53 @@[m [miBool handleCommand_App(const char *cmd) {[m }[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "export")) {[m [32m+[m[32m iExport *export = new_Export();[m [32m+[m[32m iBuffer *zip = new_Buffer();[m [32m+[m[32m generate_Export(export);[m [32m+[m[32m openEmpty_Buffer(zip);[m [32m+[m[32m serialize_Archive(archive_Export(export), stream_Buffer(zip));[m [32m+[m[32m iDocumentWidget *expTab = newTab_App(NULL, iTrue);[m [32m+[m[32m iDate now;[m [32m+[m[32m initCurrent_Date(&now);[m [32m+[m[32m setUrlAndSource_DocumentWidget([m [32m+[m[32m expTab,[m [32m+[m[32m collect_String(format_Date(&now, "file:Lagrange_User_Data_%Y-%m-%d_%H%M%S.zip")),[m [32m+[m[32m collectNewCStr_String("application/zip"),[m [32m+[m[32m data_Buffer(zip));[m [32m+[m[32m iRelease(zip);[m [32m+[m[32m delete_Export(export);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m else if (equal_Command(cmd, "import")) {[m [32m+[m[32m const iString *path = collect_String(suffix_Command(cmd, "path"));[m [32m+[m[32m iArchive *zip = iClob(new_Archive());[m [32m+[m[32m if (openFile_Archive(zip, path)) {[m [32m+[m[32m if (!arg_Command(cmd)) {[m [32m+[m[32m makeUserDataImporter_Dialog(path);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m const int bookmarks = argLabel_Command(cmd, "bookmarks");[m [32m+[m[32m const int trusted = argLabel_Command(cmd, "trusted");[m [32m+[m[32m const int idents = argLabel_Command(cmd, "idents");[m [32m+[m[32m const int visited = argLabel_Command(cmd, "visited");[m [32m+[m[32m const int siteSpec = argLabel_Command(cmd, "sitespec");[m [32m+[m[32m iExport *export = new_Export();[m [32m+[m[32m if (load_Export(export, zip)) {[m [32m+[m[32m import_Export(export, bookmarks, idents, trusted, visited, siteSpec);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.userdata.error}",[m [32m+[m[32m format_Lang("${import.userdata.error}", cstr_String(path)));[m[41m [m [32m+[m[32m }[m [32m+[m[32m delete_Export(export);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.userdata.error}",[m [32m+[m[32m format_Lang("${import.userdata.error}", cstr_String(path)));[m [32m+[m[32m }[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m #if defined (LAGRANGE_ENABLE_IPC)[m else if (equal_Command(cmd, "ipc.list.urls")) {[m iProcessId pid = argLabel_Command(cmd, "pid");[m [1mdiff --git a/src/bookmarks.c b/src/bookmarks.c[m [1mindex 500caa38..6f4b13ca 100644[m [1m--- a/src/bookmarks.c[m [1m+++ b/src/bookmarks.c[m [36m@@ -277,21 +277,43 @@[m [mstatic void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) {[m iDeclareType(BookmarkLoader)[m [m struct Impl_BookmarkLoader {[m [31m- iTomlParser *toml;[m [31m- iBookmarks * bookmarks;[m [31m- iBookmark * bm;[m [32m+[m[32m iTomlParser *toml;[m [32m+[m[32m iBookmarks *bookmarks;[m [32m+[m[32m iBookmark *bm;[m [32m+[m[32m uint32_t loadId;[m [32m+[m[32m enum iImportMethod method;[m [32m+[m[32m uint32_t baseId;[m [32m+[m[32m uint32_t dupFolderId;[m [32m+[m[32m iBool didImportDuplicates;[m };[m [m static void handleTable_BookmarkLoader_(void *context, const iString *table, iBool isStart) {[m iBookmarkLoader *d = context;[m if (isStart) {[m iAssert(!d->bm);[m [32m+[m[32m iAssert(d->method != none_ImportMethod);[m d->bm = new_Bookmark();[m [31m- const int id = toInt_String(table);[m [31m- d->bookmarks->idEnum = iMax(d->bookmarks->idEnum, id);[m [31m- insertId_Bookmarks_(d->bookmarks, d->bm, id);[m [31m- }[m [31m- else {[m [32m+[m[32m d->loadId = toInt_String(table) + d->baseId;[m [32m+[m[32m }[m [32m+[m[32m else if (d->bm) {[m [32m+[m[32m /* Check if import rules. */[m [32m+[m[32m if (d->baseId && !isFolder_Bookmark(d->bm)) {[m [32m+[m[32m const uint32_t existing = findUrl_Bookmarks(d->bookmarks, &d->bm->url);[m [32m+[m[32m if (existing) {[m [32m+[m[32m if (d->method == ifMissing_ImportMethod) {[m [32m+[m[32m /* Already have this one. */[m [32m+[m[32m delete_Bookmark(d->bm);[m [32m+[m[32m d->bm = NULL;[m [32m+[m[32m return;[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m d->bm->parentId = d->dupFolderId;[m [32m+[m[32m d->didImportDuplicates = iTrue;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m d->bookmarks->idEnum = iMax(d->bookmarks->idEnum, d->loadId);[m [32m+[m[32m insertId_Bookmarks_(d->bookmarks, d->bm, d->loadId);[m d->bm = NULL;[m }[m }[m [36m@@ -319,7 +341,7 @@[m [mstatic void handleKeyValue_BookmarkLoader_(void *context, const iString *table,[m initSeconds_Time(&bm->when, tv->value.int64);[m }[m else if (!cmp_String(key, "parent") && tv->type == int64_TomlType) {[m [31m- bm->parentId = tv->value.int64;[m [32m+[m[32m bm->parentId = tv->value.int64 + d->baseId;[m }[m else if (!cmp_String(key, "order") && tv->type == int64_TomlType) {[m bm->order = tv->value.int64;[m [36m@@ -335,16 +357,29 @@[m [mstatic void init_BookmarkLoader(iBookmarkLoader *d, iBookmarks *bookmarks) {[m setHandlers_TomlParser(d->toml, handleTable_BookmarkLoader_, handleKeyValue_BookmarkLoader_, d);[m d->bookmarks = bookmarks;[m d->bm = NULL;[m [32m+[m[32m d->loadId = 0;[m [32m+[m[32m d->method = all_ImportMethod;[m [32m+[m[32m d->baseId = bookmarks->idEnum; /* allows importing bookmarks without ID conflicts */[m [32m+[m[32m d->dupFolderId = 0;[m [32m+[m[32m d->didImportDuplicates = iFalse;[m }[m [m static void deinit_BookmarkLoader(iBookmarkLoader *d) {[m delete_TomlParser(d->toml);[m }[m [m [31m-static void load_BookmarkLoader(iBookmarkLoader *d, iFile *file) {[m [31m- if (!parse_TomlParser(d->toml, collect_String(readString_File(file)))) {[m [31m- fprintf(stderr, "[Bookmarks] syntax error(s) in %s\n", cstr_String(path_File(file)));[m [31m- } [m [32m+[m[32mstatic void load_BookmarkLoader(iBookmarkLoader *d, iStream *stream) {[m [32m+[m[32m if (d->baseId && d->method == all_ImportMethod) {[m [32m+[m[32m /* Make a folder for possible duplicate bookmarks. */[m [32m+[m[32m d->dupFolderId =[m [32m+[m[32m add_Bookmarks(d->bookmarks, NULL, string_Lang("import.userdata.dupfolder"), NULL, 0);[m [32m+[m[32m }[m [32m+[m[32m if (!parse_TomlParser(d->toml, collect_String(readString_Stream(stream)))) {[m [32m+[m[32m fprintf(stderr, "[Bookmarks] syntax error in bookmarks.ini\n");[m [32m+[m[32m }[m [32m+[m[32m if (d->dupFolderId && !d->didImportDuplicates) {[m [32m+[m[32m remove_Bookmarks(d->bookmarks, d->dupFolderId);[m [32m+[m[32m }[m }[m [m iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)[m [36m@@ -364,6 +399,46 @@[m [mvoid sort_Bookmarks(iBookmarks *d, uint32_t parentId, iBookmarksCompareFunc cmp)[m unlock_Mutex(d->mtx);[m }[m [m [32m+[m[32mstatic void mergeFolders_BookmarkLoader(iBookmarkLoader *d) {[m [32m+[m[32m if (!d->baseId) {[m [32m+[m[32m /* Only merge after importing. */[m [32m+[m[32m return;[m [32m+[m[32m }[m [32m+[m[32m iHash *hash = &d->bookmarks->bookmarks;[m [32m+[m[32m iForEach(Hash, i, hash) {[m [32m+[m[32m iBookmark *imported = (iBookmark *) i.value;[m [32m+[m[32m if (isFolder_Bookmark(imported) && id_Bookmark(imported) >= d->baseId) {[m [32m+[m[32m /* If there already is a folder with a matching name, merge this one into it. */[m [32m+[m[32m iForEach(Hash, j, hash) {[m [32m+[m[32m iBookmark *old = (iBookmark *) j.value;[m [32m+[m[32m if (isFolder_Bookmark(old) && id_Bookmark(old) < d->baseId &&[m [32m+[m[32m equal_String(&imported->title, &old->title)) {[m [32m+[m[32m iForEach(Hash, k, hash) {[m [32m+[m[32m iBookmark *bm = (iBookmark *) k.value;[m [32m+[m[32m if (bm->parentId == id_Bookmark(imported)) {[m [32m+[m[32m bm->parentId = id_Bookmark(old);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m remove_HashIterator(&i);[m [32m+[m[32m delete_Bookmark(imported);[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid deserialize_Bookmarks(iBookmarks *d, iStream *ins, enum iImportMethod method) {[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m iBookmarkLoader loader;[m [32m+[m[32m init_BookmarkLoader(&loader, d);[m [32m+[m[32m loader.method = method;[m [32m+[m[32m load_BookmarkLoader(&loader, ins);[m [32m+[m[32m mergeFolders_BookmarkLoader(&loader);[m [32m+[m[32m deinit_BookmarkLoader(&loader);[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m}[m [32m+[m void load_Bookmarks(iBookmarks *d, const char *dirPath) {[m clear_Bookmarks(d);[m /* Load new .ini bookmarks, if present. */[m [36m@@ -377,49 +452,53 @@[m [mvoid load_Bookmarks(iBookmarks *d, const char *dirPath) {[m }[m iBookmarkLoader loader;[m init_BookmarkLoader(&loader, d);[m [31m- load_BookmarkLoader(&loader, f);[m [32m+[m[32m load_BookmarkLoader(&loader, stream_File(f));[m deinit_BookmarkLoader(&loader);[m }[m [m [32m+[m[32mvoid serialize_Bookmarks(const iBookmarks *d, iStream *out) {[m [32m+[m[32m iString *str = collectNew_String();[m [32m+[m[32m format_String(str, "recentfolder = %u\n\n", d->recentFolderId);[m [32m+[m[32m writeData_Stream(out, cstr_String(str), size_String(str));[m [32m+[m[32m iConstForEach(Hash, i, &d->bookmarks) {[m [32m+[m[32m const iBookmark *bm = (const iBookmark *) i.value;[m [32m+[m[32m if (bm->flags & remote_BookmarkFlag) {[m [32m+[m[32m /* Remote bookmarks are not saved. */[m [32m+[m[32m continue;[m [32m+[m[32m }[m [32m+[m[32m iBeginCollect();[m [32m+[m[32m const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm));[m [32m+[m[32m format_String(str,[m [32m+[m[32m "[%d]\n"[m [32m+[m[32m "url = \"%s\"\n"[m [32m+[m[32m "title = \"%s\"\n"[m [32m+[m[32m "tags = \"%s\"\n"[m [32m+[m[32m "icon = 0x%x\n"[m [32m+[m[32m "created = %.0f # %s\n",[m [32m+[m[32m id_Bookmark(bm),[m [32m+[m[32m cstrCollect_String(quote_String(&bm->url, iFalse)),[m [32m+[m[32m cstrCollect_String(quote_String(&bm->title, iFalse)),[m [32m+[m[32m cstrCollect_String(quote_String(packedTags, iFalse)),[m [32m+[m[32m bm->icon,[m [32m+[m[32m seconds_Time(&bm->when),[m [32m+[m[32m cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));[m [32m+[m[32m if (bm->parentId) {[m [32m+[m[32m appendFormat_String(str, "parent = %d\n", bm->parentId);[m [32m+[m[32m }[m [32m+[m[32m if (bm->order) {[m [32m+[m[32m appendFormat_String(str, "order = %d\n", bm->order);[m [32m+[m[32m }[m [32m+[m[32m appendCStr_String(str, "\n");[m [32m+[m[32m writeData_Stream(out, cstr_String(str), size_String(str));[m [32m+[m[32m iEndCollect();[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m void save_Bookmarks(const iBookmarks *d, const char *dirPath) {[m lock_Mutex(d->mtx);[m iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_));[m [31m- if (open_File(f, writeOnly_FileMode | text_FileMode)) { [m [31m- iString *str = collectNew_String();[m [31m- format_String(str, "recentfolder = %u\n\n", d->recentFolderId);[m [31m- writeData_File(f, cstr_String(str), size_String(str));[m [31m- iConstForEach(Hash, i, &d->bookmarks) {[m [31m- const iBookmark *bm = (const iBookmark *) i.value;[m [31m- if (bm->flags & remote_BookmarkFlag) {[m [31m- /* Remote bookmarks are not saved. */[m [31m- continue;[m [31m- }[m [31m- iBeginCollect();[m [31m- const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm));[m [31m- format_String(str,[m [31m- "[%d]\n"[m [31m- "url = \"%s\"\n"[m [31m- "title = \"%s\"\n"[m [31m- "tags = \"%s\"\n"[m [31m- "icon = 0x%x\n"[m [31m- "created = %.0f # %s\n",[m [31m- id_Bookmark(bm),[m [31m- cstrCollect_String(quote_String(&bm->url, iFalse)),[m [31m- cstrCollect_String(quote_String(&bm->title, iFalse)),[m [31m- cstrCollect_String(quote_String(packedTags, iFalse)),[m [31m- bm->icon,[m [31m- seconds_Time(&bm->when),[m [31m- cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));[m [31m- if (bm->parentId) {[m [31m- appendFormat_String(str, "parent = %d\n", bm->parentId);[m [31m- }[m [31m- if (bm->order) {[m [31m- appendFormat_String(str, "order = %d\n", bm->order);[m [31m- }[m [31m- appendCStr_String(str, "\n");[m [31m- writeData_File(f, cstr_String(str), size_String(str));[m [31m- iEndCollect();[m [31m- } [m [32m+[m[32m if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [32m+[m[32m serialize_Bookmarks(d, stream_File(f));[m }[m iRelease(f);[m unlock_Mutex(d->mtx);[m [1mdiff --git a/src/bookmarks.h b/src/bookmarks.h[m [1mindex 08afdd8b..13a93748 100644[m [1m--- a/src/bookmarks.h[m [1m+++ b/src/bookmarks.h[m [36m@@ -22,6 +22,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #pragma once[m [m [32m+[m[32m#include "defs.h"[m #include [m #include [m #include [m [36m@@ -97,6 +98,8 @@[m [mtypedef int (*iBookmarksCompareFunc) (const iBookmark **, const iBookmark **)[m void clear_Bookmarks (iBookmarks *);[m void load_Bookmarks (iBookmarks *, const char *dirPath);[m void save_Bookmarks (const iBookmarks *, const char *dirPath);[m [32m+[m[32mvoid serialize_Bookmarks (const iBookmarks *, iStream *outs);[m [32m+[m[32mvoid deserialize_Bookmarks (iBookmarks *, iStream *ins, enum iImportMethod);[m [m uint32_t add_Bookmarks (iBookmarks *, const iString *url, const iString *title,[m const iString *tags, iChar icon);[m [1mdiff --git a/src/defs.h b/src/defs.h[m [1mindex fed7fe82..93db7fa0 100644[m [1m--- a/src/defs.h[m [1m+++ b/src/defs.h[m [36m@@ -31,6 +31,12 @@[m [menum iSourceFormat {[m markdown_SourceFormat,[m };[m [m [32m+[m[32menum iImportMethod {[m [32m+[m[32m none_ImportMethod = 0,[m [32m+[m[32m ifMissing_ImportMethod,[m [32m+[m[32m all_ImportMethod,[m [32m+[m[32m};[m [32m+[m enum iFileVersion {[m initial_FileVersion = 0,[m addedResponseTimestamps_FileVersion = 1,[m [36m@@ -127,7 +133,8 @@[m [miLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {[m #define backArrow_Icon "\U0001f870"[m #define forwardArrow_Icon "\U0001f872"[m #define upArrow_Icon "\U0001f871"[m [31m-#define upArrowBar_Icon "\u2912"[m [32m+[m[32m#define upArrowBar_Icon "\u2b71"[m [32m+[m[32m#define keyUpArrow_Icon "\u2191"[m #define downArrowBar_Icon "\u2913"[m #define rightArrowWhite_Icon "\u21e8"[m #define rightArrow_Icon "\u279e"[m [36m@@ -152,7 +159,7 @@[m [miLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {[m #define check_Icon "\u2714"[m #define ballotChecked_Icon "\u2611"[m #define ballotUnchecked_Icon "\u2610"[m [31m-#define inbox_Icon "\U0001f4e5"[m [32m+[m[32m#define import_Icon "\U0001f4e5"[m #define book_Icon "\U0001f56e"[m #define bookmark_Icon "\U0001f516"[m #define folder_Icon "\U0001f4c1"[m [1mdiff --git a/src/export.c b/src/export.c[m [1mnew file mode 100644[m [1mindex 00000000..a343020a[m [1m--- /dev/null[m [1m+++ b/src/export.c[m [36m@@ -0,0 +1,205 @@[m [32m+[m[32m/* Copyright 2022 Jaakko Keränen [m [32m+[m [32m+[m[32mRedistribution and use in source and binary forms, with or without[m [32m+[m[32mmodification, are permitted provided that the following conditions are met:[m [32m+[m [32m+[m[32m1. Redistributions of source code must retain the above copyright notice, this[m [32m+[m[32m list of conditions and the following disclaimer.[m [32m+[m[32m2. Redistributions in binary form must reproduce the above copyright notice,[m [32m+[m[32m this list of conditions and the following disclaimer in the documentation[m [32m+[m[32m and/or other materials provided with the distribution.[m [32m+[m [32m+[m[32mTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND[m [32m+[m[32mANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED[m [32m+[m[32mWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE[m [32m+[m[32mDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR[m [32m+[m[32mANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES[m [32m+[m[32m(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;[m [32m+[m[32mLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON[m [32m+[m[32mANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT[m [32m+[m[32m(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS[m [32m+[m[32mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [32m+[m [32m+[m[32m#include "export.h"[m [32m+[m [32m+[m[32m#include "app.h"[m [32m+[m[32m#include "bookmarks.h"[m [32m+[m[32m#include "gmcerts.h"[m [32m+[m[32m#include "sitespec.h"[m [32m+[m[32m#include "visited.h"[m [32m+[m [32m+[m[32m#include [m [32m+[m[32m#include [m [32m+[m[32m#include [m [32m+[m[32m#include [m [32m+[m[32m#include [m [32m+[m [32m+[m[32mconst char *mimeType_Export = "application/lagrange-export+zip";[m [32m+[m [32m+[m[32mstruct Impl_Export {[m [32m+[m[32m iArchive *arch;[m [32m+[m[32m};[m [32m+[m [32m+[m[32miDefineTypeConstruction(Export)[m [32m+[m[41m [m [32m+[m[32mstatic const char *metadataEntryName_Export_ = "lagrange-export.ini";[m [32m+[m[41m [m [32m+[m[32mvoid init_Export(iExport *d) {[m [32m+[m[32m d->arch = new_Archive();[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid deinit_Export(iExport *d) {[m [32m+[m[32m iRelease(d->arch);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid generate_Export(iExport *d) {[m [32m+[m[32m openWritable_Archive(d->arch);[m [32m+[m[32m iBuffer *buf = new_Buffer();[m [32m+[m[32m iString *meta = new_String();[m [32m+[m[32m iDate today;[m [32m+[m[32m iTime now;[m [32m+[m[32m initCurrent_Date(&today);[m [32m+[m[32m initCurrent_Time(&now);[m [32m+[m[32m format_String(meta,[m [32m+[m[32m "# Lagrange user data exported on %s\n"[m [32m+[m[32m "version = \"" LAGRANGE_APP_VERSION "\"\n"[m [32m+[m[32m "timestamp = %llu\n",[m [32m+[m[32m cstrCollect_String(format_Date(&today, "%Y-%m-%d %H:%M")),[m [32m+[m[32m (unsigned long long) integralSeconds_Time(&now));[m [32m+[m[32m /* Bookmarks. */ {[m [32m+[m[32m openEmpty_Buffer(buf);[m [32m+[m[32m serialize_Bookmarks(bookmarks_App(), stream_Buffer(buf));[m [32m+[m[32m setDataCStr_Archive(d->arch, "bookmarks.ini", data_Buffer(buf));[m [32m+[m[32m close_Buffer(buf);[m [32m+[m[32m }[m [32m+[m[32m /* Identities. */ {[m [32m+[m[32m iBuffer *buf2 = new_Buffer();[m [32m+[m[32m openEmpty_Buffer(buf2);[m [32m+[m[32m openEmpty_Buffer(buf);[m [32m+[m[32m serialize_GmCerts(certs_App(), stream_Buffer(buf), stream_Buffer(buf2));[m [32m+[m[32m setDataCStr_Archive(d->arch, "trusted.txt", data_Buffer(buf));[m [32m+[m[32m setDataCStr_Archive(d->arch, "idents.lgr", data_Buffer(buf2));[m [32m+[m[32m iRelease(buf2);[m [32m+[m[32m iForEach(DirFileInfo,[m [32m+[m[32m info,[m [32m+[m[32m iClob(new_DirFileInfo(collect_String(concatCStr_Path(dataDir_App(), "idents"))))) {[m [32m+[m[32m const iString *idPath = path_FileInfo(info.value);[m [32m+[m[32m const iRangecc baseName = baseName_Path(idPath);[m [32m+[m[32m if (!startsWith_Rangecc(baseName, ".") &&[m [32m+[m[32m (endsWith_Rangecc(baseName, ".crt") || endsWith_Rangecc(baseName, ".key"))) {[m [32m+[m[32m iFile *f = new_File(idPath);[m [32m+[m[32m if (open_File(f, readOnly_FileMode)) {[m [32m+[m[32m setData_Archive(d->arch,[m [32m+[m[32m collectNewFormat_String("idents/%s", cstr_Rangecc(baseName)),[m [32m+[m[32m collect_Block(readAll_File(f)));[m [32m+[m[32m }[m [32m+[m[32m iRelease(f);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m close_Buffer(buf);[m [32m+[m[32m }[m [32m+[m[32m /* Site-specific settings. */ {[m [32m+[m[32m openEmpty_Buffer(buf);[m [32m+[m[32m serialize_SiteSpec(stream_Buffer(buf));[m [32m+[m[32m setDataCStr_Archive(d->arch, "sitespec.ini", data_Buffer(buf));[m [32m+[m[32m close_Buffer(buf);[m [32m+[m[32m }[m [32m+[m[32m /* History of visited URLs. */ {[m [32m+[m[32m openEmpty_Buffer(buf);[m [32m+[m[32m serialize_Visited(visited_App(), stream_Buffer(buf));[m [32m+[m[32m setDataCStr_Archive(d->arch, "visited.txt", data_Buffer(buf));[m [32m+[m[32m close_Buffer(buf);[m [32m+[m[32m }[m[41m [m [32m+[m[32m /* Export metadata. */[m [32m+[m[32m setDataCStr_Archive(d->arch, metadataEntryName_Export_, utf8_String(meta));[m [32m+[m[32m delete_String(meta);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m}[m [32m+[m [32m+[m[32miBool load_Export(iExport *d, const iArchive *archive) {[m [32m+[m[32m if (!detect_Export(archive)) {[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m [32m+[m[32m iRelease(d->arch);[m [32m+[m[32m d->arch = ref_Object(archive);[m [32m+[m[32m /* TODO: Check that at least one of the expected files is there. */[m [32m+[m[32m return iTrue;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miBuffer *openEntryBuffer_Export_(const iExport *d, const char *entryPath) {[m [32m+[m[32m iBuffer *buf = new_Buffer();[m [32m+[m[32m if (open_Buffer(buf, dataCStr_Archive(d->arch, entryPath))) {[m [32m+[m[32m return buf;[m [32m+[m[32m }[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m return NULL;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid import_Export(const iExport *d, enum iImportMethod bookmarks, enum iImportMethod identities,[m [32m+[m[32m enum iImportMethod trusted, enum iImportMethod visited,[m [32m+[m[32m enum iImportMethod siteSpec) {[m [32m+[m[32m if (bookmarks) {[m [32m+[m[32m iBuffer *buf = openEntryBuffer_Export_(d, "bookmarks.ini");[m [32m+[m[32m if (buf) {[m [32m+[m[32m deserialize_Bookmarks(bookmarks_App(), stream_Buffer(buf), bookmarks);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m postCommand_App("bookmarks.changed");[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (trusted) {[m [32m+[m[32m iBuffer *buf = openEntryBuffer_Export_(d, "trusted.txt");[m [32m+[m[32m if (buf) {[m [32m+[m[32m deserializeTrusted_GmCerts(certs_App(), stream_Buffer(buf), trusted);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (identities) {[m [32m+[m[32m /* First extract any missing .crt/.key files to the idents directory. */[m [32m+[m[32m const iString *identsDir = collect_String(concatCStr_Path(dataDir_App(), "idents"));[m [32m+[m[32m iConstForEach(StringSet, i,[m [32m+[m[32m iClob(listDirectory_Archive(d->arch, collectNewCStr_String("idents/")))) {[m [32m+[m[32m iString *dataPath = concatCStr_Path(identsDir, cstr_Rangecc(baseName_Path(i.value)));[m [32m+[m[32m if (identities == all_ImportMethod || !fileExists_FileInfo(dataPath)) {[m [32m+[m[32m iFile *f = new_File(dataPath);[m [32m+[m[32m if (open_File(f, writeOnly_FileMode)) {[m [32m+[m[32m write_File(f, data_Archive(d->arch, i.value));[m [32m+[m[32m }[m [32m+[m[32m iRelease(f);[m [32m+[m[32m }[m [32m+[m[32m delete_String(dataPath);[m [32m+[m[32m }[m [32m+[m[32m iBuffer *buf = openEntryBuffer_Export_(d, "idents.lgr");[m [32m+[m[32m if (buf) {[m [32m+[m[32m deserializeIdentities_GmCerts(certs_App(), stream_Buffer(buf), identities);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m postCommand_App("idents.changed");[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (visited) {[m [32m+[m[32m iBuffer *buf = openEntryBuffer_Export_(d, "visited.txt");[m [32m+[m[32m if (buf) {[m [32m+[m[32m deserialize_Visited(visited_App(), stream_Buffer(buf), iTrue /* keep latest */);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m postCommand_App("visited.changed");[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (siteSpec) {[m [32m+[m[32m iBuffer *buf = openEntryBuffer_Export_(d, "sitespec.ini");[m [32m+[m[32m if (buf) {[m [32m+[m[32m deserialize_SiteSpec(stream_Buffer(buf), siteSpec);[m [32m+[m[32m iRelease(buf);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m [32m+[m[32miBool detect_Export(const iArchive *d) {[m [32m+[m[32m if (entryCStr_Archive(d, metadataEntryName_Export_)) {[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m /* TODO: Additional checks? */[m [32m+[m[32m return iFalse;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mconst iArchive *archive_Export(const iExport *d) {[m [32m+[m[32m return d->arch;[m [32m+[m[32m}[m [1mdiff --git a/src/export.h b/src/export.h[m [1mnew file mode 100644[m [1mindex 00000000..149a08c8[m [1m--- /dev/null[m [1m+++ b/src/export.h[m [36m@@ -0,0 +1,44 @@[m [32m+[m[32m/* Copyright 2022 Jaakko Keränen [m [32m+[m [32m+[m[32mRedistribution and use in source and binary forms, with or without[m [32m+[m[32mmodification, are permitted provided that the following conditions are met:[m [32m+[m [32m+[m[32m1. Redistributions of source code must retain the above copyright notice, this[m [32m+[m[32m list of conditions and the following disclaimer.[m [32m+[m[32m2. Redistributions in binary form must reproduce the above copyright notice,[m [32m+[m[32m this list of conditions and the following disclaimer in the documentation[m [32m+[m[32m and/or other materials provided with the distribution.[m [32m+[m [32m+[m[32mTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND[m [32m+[m[32mANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED[m [32m+[m[32mWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE[m [32m+[m[32mDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR[m [32m+[m[32mANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES[m [32m+[m[32m(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;[m [32m+[m[32mLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON[m [32m+[m[32mANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT[m [32m+[m[32m(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS[m [32m+[m[32mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [32m+[m [32m+[m[32m#pragma once[m [32m+[m [32m+[m[32m#include "defs.h"[m [32m+[m[32m#include [m [32m+[m [32m+[m[32mextern const char *mimeType_Export;[m [32m+[m [32m+[m[32miDeclareType(Export)[m [32m+[m[32miDeclareTypeConstruction(Export)[m[41m [m [32m+[m[41m [m [32m+[m[32mvoid generate_Export (iExport *);[m [32m+[m[32miBool load_Export (iExport *, const iArchive *archive);[m [32m+[m[32mvoid import_Export (const iExport *,[m [32m+[m[32m enum iImportMethod bookmarks,[m [32m+[m[32m enum iImportMethod identities,[m [32m+[m[32m enum iImportMethod trusted,[m [32m+[m[32m enum iImportMethod visited,[m [32m+[m[32m enum iImportMethod siteSpec);[m [32m+[m [32m+[m[32miBool detect_Export (const iArchive *);[m [32m+[m [32m+[m[32mconst iArchive * archive_Export (const iExport *);[m [1mdiff --git a/src/gmcerts.c b/src/gmcerts.c[m [1mindex 7b05103b..dc36cb59 100644[m [1m--- a/src/gmcerts.c[m [1m+++ b/src/gmcerts.c[m [36m@@ -36,7 +36,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include [m #include [m [m [31m-static const char *filename_GmCerts_ = "trusted.2.txt";[m [32m+[m[32mstatic const char *trustedFilename_GmCerts_ = "trusted.2.txt";[m static const char *identsDir_GmCerts_ = "idents";[m static const char *oldIdentsFilename_GmCerts_ = "idents.binary";[m static const char *identsFilename_GmCerts_ = "idents.lgr";[m [36m@@ -248,26 +248,8 @@[m [mstatic const char *magicIdentity_GmCerts_ = "iden";[m [m iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir)[m [m [31m-void saveIdentities_GmCerts(const iGmCerts *d) {[m [31m- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)));[m [31m- if (open_File(f, writeOnly_FileMode)) {[m [31m- writeData_File(f, magicIdMeta_GmCerts_, 4);[m [31m- writeU32_File(f, idents_FileVersion); /* version */[m [31m- iConstForEach(PtrArray, i, &d->idents) {[m [31m- const iGmIdentity *ident = i.ptr;[m [31m- if (~ident->flags & temporary_GmIdentityFlag) {[m [31m- writeData_File(f, magicIdentity_GmCerts_, 4);[m [31m- serialize_GmIdentity(ident, stream_File(f));[m [31m- }[m [31m- }[m [31m- }[m [31m- iRelease(f);[m [31m-}[m [31m-[m [31m-static void save_GmCerts_(const iGmCerts *d) {[m [31m- iBeginCollect();[m [31m- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));[m [31m- if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [32m+[m[32mvoid serialize_GmCerts(const iGmCerts *d, iStream *trusted, iStream *identsMeta) {[m [32m+[m[32m if (trusted) {[m iString line;[m init_String(&line);[m iConstForEach(StringHash, i, d->trusted) {[m [36m@@ -277,57 +259,39 @@[m [mstatic void save_GmCerts_(const iGmCerts *d) {[m cstr_String(key_StringHashConstIterator(&i)),[m integralSeconds_Time(&trust->validUntil),[m cstrCollect_String(hexEncode_Block(&trust->fingerprint)));[m [31m- write_File(f, &line.chars);[m [32m+[m[32m write_Stream(trusted, &line.chars);[m }[m [31m- deinit_String(&line);[m [32m+[m[32m deinit_String(&line);[m[41m [m }[m [31m- iRelease(f);[m [31m- iEndCollect();[m [31m-}[m [31m-[m [31m-static void loadIdentities_GmCerts_(iGmCerts *d) {[m [31m- const iString *oldPath = collect_String(concatCStr_Path(&d->saveDir, oldIdentsFilename_GmCerts_));[m [31m- const iString *path = collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_));[m [31m- iFile *f = iClob(new_File(fileExists_FileInfo(path) ? path : oldPath));[m [31m- if (open_File(f, readOnly_FileMode)) {[m [31m- char magic[4];[m [31m- readData_File(f, sizeof(magic), magic);[m [31m- if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) {[m [31m- printf("%s: format not recognized\n", cstr_String(path_File(f)));[m [31m- return;[m [31m- }[m [31m- const uint32_t version = readU32_File(f);[m [31m- if (version > latest_FileVersion) {[m [31m- printf("%s: unsupported version\n", cstr_String(path_File(f)));[m [31m- return;[m [31m- }[m [31m- setVersion_Stream(stream_File(f), version);[m [31m- while (!atEnd_File(f)) {[m [31m- readData_File(f, sizeof(magic), magic);[m [31m- if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) {[m [31m- iGmIdentity *id = new_GmIdentity();[m [31m- deserialize_GmIdentity(id, stream_File(f));[m [31m- pushBack_PtrArray(&d->idents, id);[m [31m- }[m [31m- else {[m [31m- printf("%s: invalid file contents\n", cstr_String(path_File(f)));[m [31m- break;[m [32m+[m[32m if (identsMeta) {[m [32m+[m[32m writeData_Stream(identsMeta, magicIdMeta_GmCerts_, 4);[m [32m+[m[32m writeU32_Stream(identsMeta, idents_FileVersion); /* version */[m [32m+[m[32m iConstForEach(PtrArray, i, &d->idents) {[m [32m+[m[32m const iGmIdentity *ident = i.ptr;[m [32m+[m[32m if (~ident->flags & temporary_GmIdentityFlag) {[m [32m+[m[32m writeData_Stream(identsMeta, magicIdentity_GmCerts_, 4);[m [32m+[m[32m serialize_GmIdentity(ident, identsMeta);[m }[m [31m- }[m [32m+[m[32m }[m[41m [m }[m }[m [m [31m-iGmIdentity *findIdentity_GmCerts(iGmCerts *d, const iBlock *fingerprint) {[m [31m- if (isEmpty_Block(fingerprint)) {[m [31m- return NULL;[m [32m+[m[32mvoid saveIdentities_GmCerts(const iGmCerts *d) {[m [32m+[m[32m iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)));[m [32m+[m[32m if (open_File(f, writeOnly_FileMode)) {[m [32m+[m[32m serialize_GmCerts(d, NULL, stream_File(f));[m }[m [31m- iForEach(PtrArray, i, &d->idents) {[m [31m- iGmIdentity *ident = i.ptr;[m [31m- if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */[m [31m- return ident;[m [31m- }[m [32m+[m[32m iRelease(f);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void save_GmCerts_(const iGmCerts *d) {[m [32m+[m[32m iBeginCollect();[m [32m+[m[32m iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, trustedFilename_GmCerts_)));[m [32m+[m[32m if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [32m+[m[32m serialize_GmCerts(d, stream_File(f), NULL);[m[41m [m }[m [31m- return NULL;[m [32m+[m[32m iRelease(f);[m [32m+[m[32m iEndCollect();[m }[m [m static void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crtPath) {[m [36m@@ -354,53 +318,127 @@[m [mstatic void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crt[m delete_Block(finger);[m }[m [m [31m-static void load_GmCerts_(iGmCerts *d) {[m [31m- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));[m [31m- if (open_File(f, readOnly_FileMode | text_FileMode)) {[m [31m- iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0);[m [31m- const iRangecc src = range_Block(collect_Block(readAll_File(f)));[m [31m- iRangecc line = iNullRange;[m [31m- while (nextSplit_Rangecc(src, "\n", &line)) {[m [31m- iRegExpMatch m;[m [31m- init_RegExpMatch(&m);[m [31m- if (matchRange_RegExp(pattern, line, &m)) {[m [31m- const iRangecc key = capturedRange_RegExpMatch(&m, 1);[m [31m- const iRangecc until = capturedRange_RegExpMatch(&m, 2);[m [31m- const iRangecc fp = capturedRange_RegExpMatch(&m, 3);[m [31m- time_t sec;[m [31m- sscanf(until.start, "%ld", &sec);[m [31m- iDate untilDate;[m [31m- initSinceEpoch_Date(&untilDate, sec);[m [31m- insert_StringHash(d->trusted,[m [31m- collect_String(newRange_String(key)),[m [31m- new_TrustEntry(collect_Block(hexDecode_Rangecc(fp)),[m [31m- &untilDate));[m [31m- }[m [32m+[m[32mstatic void loadIdentityCertsAndDiscardInvalid_GmCerts_(iGmCerts *d) {[m [32m+[m[32m const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_));[m [32m+[m[32m if (!fileExists_FileInfo(idDir)) {[m [32m+[m[32m makeDirs_Path(idDir);[m [32m+[m[32m }[m [32m+[m[32m iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) {[m [32m+[m[32m const iFileInfo *entry = i.value;[m [32m+[m[32m if (endsWithCase_String(path_FileInfo(entry), ".crt")) {[m [32m+[m[32m loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry));[m }[m [31m- iRelease(pattern);[m }[m [31m- iRelease(f);[m [31m- /* Load all identity certificates. */ {[m [31m- loadIdentities_GmCerts_(d);[m [31m- const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_));[m [31m- if (!fileExists_FileInfo(idDir)) {[m [31m- makeDirs_Path(idDir);[m [32m+[m[32m /* Remove certificates whose crt/key files were missing. */[m [32m+[m[32m iForEach(PtrArray, j, &d->idents) {[m [32m+[m[32m iGmIdentity *ident = j.ptr;[m [32m+[m[32m if (!isValid_GmIdentity_(ident)) {[m [32m+[m[32m delete_GmIdentity(ident);[m [32m+[m[32m remove_PtrArrayIterator(&j);[m }[m [31m- iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) {[m [31m- const iFileInfo *entry = i.value;[m [31m- if (endsWithCase_String(path_FileInfo(entry), ".crt")) {[m [31m- loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry));[m [32m+[m[32m }[m[41m [m [32m+[m[32m}[m [32m+[m [32m+[m[32miBool deserializeIdentities_GmCerts(iGmCerts *d, iStream *ins, enum iImportMethod method) {[m [32m+[m[32m char magic[4];[m [32m+[m[32m readData_Stream(ins, sizeof(magic), magic);[m [32m+[m[32m if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) {[m [32m+[m[32m fprintf(stderr, "[GmCerts] idents file format not recognized\n");[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m [32m+[m[32m const uint32_t version = readU32_Stream(ins);[m [32m+[m[32m if (version > latest_FileVersion) {[m [32m+[m[32m fprintf(stderr, "[GmCerts] unsupported version (%u)\n", version);[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m [32m+[m[32m setVersion_Stream(ins, version);[m [32m+[m[32m while (!atEnd_Stream(ins)) {[m [32m+[m[32m readData_Stream(ins, sizeof(magic), magic);[m [32m+[m[32m if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) {[m [32m+[m[32m iGmIdentity *id = new_GmIdentity();[m [32m+[m[32m deserialize_GmIdentity(id, ins);[m [32m+[m[32m if (method == all_ImportMethod ||[m [32m+[m[32m (method == ifMissing_ImportMethod && !findIdentity_GmCerts(d, &id->fingerprint))) {[m [32m+[m[32m pushBack_PtrArray(&d->idents, id);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m delete_GmIdentity(id);[m }[m }[m [31m- /* Remove certificates whose crt/key files were missing. */[m [31m- iForEach(PtrArray, j, &d->idents) {[m [31m- iGmIdentity *ident = j.ptr;[m [31m- if (!isValid_GmIdentity_(ident)) {[m [31m- delete_GmIdentity(ident);[m [31m- remove_PtrArrayIterator(&j);[m [32m+[m[32m else {[m [32m+[m[32m fprintf(stderr, "[GmCerts] invalid idents file\n");[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m loadIdentityCertsAndDiscardInvalid_GmCerts_(d);[m [32m+[m[32m return iTrue;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void loadIdentities_GmCerts_(iGmCerts *d) {[m [32m+[m[32m const iString *oldPath = collect_String(concatCStr_Path(&d->saveDir, oldIdentsFilename_GmCerts_));[m [32m+[m[32m const iString *path = collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_));[m [32m+[m[32m iFile *f = iClob(new_File(fileExists_FileInfo(path) ? path : oldPath));[m [32m+[m[32m if (open_File(f, readOnly_FileMode)) {[m [32m+[m[32m deserializeIdentities_GmCerts(d, stream_File(f), all_ImportMethod);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m /* In any case, load any .crt/.key files that may be present in the "idents" dir. */[m [32m+[m[32m loadIdentityCertsAndDiscardInvalid_GmCerts_(d);[m[41m [m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m [32m+[m[32miGmIdentity *findIdentity_GmCerts(iGmCerts *d, const iBlock *fingerprint) {[m [32m+[m[32m if (isEmpty_Block(fingerprint)) {[m [32m+[m[32m return NULL;[m [32m+[m[32m }[m [32m+[m[32m iForEach(PtrArray, i, &d->idents) {[m [32m+[m[32m iGmIdentity *ident = i.ptr;[m [32m+[m[32m if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */[m [32m+[m[32m return ident;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return NULL;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid deserializeTrusted_GmCerts(iGmCerts *d, iStream *ins, enum iImportMethod method) {[m [32m+[m[32m iRegExp * pattern = new_RegExp("([^\\s]+) ([0-9]+) ([a-z0-9]+)", 0);[m [32m+[m[32m const iRangecc src = range_Block(collect_Block(readAll_Stream(ins)));[m [32m+[m[32m iRangecc line = iNullRange;[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m while (nextSplit_Rangecc(src, "\n", &line)) {[m [32m+[m[32m iRegExpMatch m;[m [32m+[m[32m init_RegExpMatch(&m);[m [32m+[m[32m if (matchRange_RegExp(pattern, line, &m)) {[m [32m+[m[32m iBeginCollect();[m [32m+[m[32m const iRangecc key = capturedRange_RegExpMatch(&m, 1);[m [32m+[m[32m const iRangecc until = capturedRange_RegExpMatch(&m, 2);[m [32m+[m[32m const iRangecc fp = capturedRange_RegExpMatch(&m, 3);[m [32m+[m[32m time_t sec;[m [32m+[m[32m sscanf(until.start, "%ld", &sec);[m [32m+[m[32m iDate untilDate;[m [32m+[m[32m initSinceEpoch_Date(&untilDate, sec);[m [32m+[m[32m /* TODO: import method? */[m [32m+[m[32m const iString *hashKey = collect_String(newRange_String(key));[m [32m+[m[32m if (method == all_ImportMethod ||[m [32m+[m[32m (method == ifMissing_ImportMethod && !contains_StringHash(d->trusted, hashKey))) {[m [32m+[m[32m insert_StringHash(d->trusted,[m [32m+[m[32m hashKey,[m [32m+[m[32m new_TrustEntry(collect_Block(hexDecode_Rangecc(fp)), &untilDate));[m }[m [32m+[m[32m iEndCollect();[m }[m }[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m iRelease(pattern);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void load_GmCerts_(iGmCerts *d) {[m [32m+[m[32m iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, trustedFilename_GmCerts_)));[m [32m+[m[32m if (open_File(f, readOnly_FileMode | text_FileMode)) {[m [32m+[m[32m deserializeTrusted_GmCerts(d, stream_File(f), all_ImportMethod);[m [32m+[m[32m }[m [32m+[m[32m iRelease(f);[m [32m+[m[32m loadIdentities_GmCerts_(d);[m }[m [m iBool verify_GmCerts_(iTlsRequest *request, const iTlsCertificate *cert, int depth) {[m [1mdiff --git a/src/gmcerts.h b/src/gmcerts.h[m [1mindex 6ece1954..b451a690 100644[m [1m--- a/src/gmcerts.h[m [1m+++ b/src/gmcerts.h[m [36m@@ -22,6 +22,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #pragma once[m [m [32m+[m[32m#include "defs.h"[m #include [m #include [m #include [m [36m@@ -87,6 +88,9 @@[m [mvoid importIdentity_GmCerts (iGmCerts *, iTlsCertificate *cert,[m const iString *notes); /* takes ownership */[m void deleteIdentity_GmCerts (iGmCerts *, iGmIdentity *identity);[m void saveIdentities_GmCerts (const iGmCerts *);[m [32m+[m[32mvoid serialize_GmCerts (const iGmCerts *, iStream *trusted, iStream *identsMeta);[m [32m+[m[32mvoid deserializeTrusted_GmCerts (iGmCerts *, iStream *ins, enum iImportMethod method);[m [32m+[m[32miBool deserializeIdentities_GmCerts (iGmCerts *, iStream *ins, enum iImportMethod method);[m [m const iString * certificatePath_GmCerts (const iGmCerts *, const iGmIdentity *identity);[m [m [1mdiff --git a/src/gmrequest.c b/src/gmrequest.c[m [1mindex 55b7b471..aa55aca2 100644[m [1m--- a/src/gmrequest.c[m [1m+++ b/src/gmrequest.c[m [36m@@ -711,7 +711,7 @@[m [mvoid submit_GmRequest(iGmRequest *d) {[m iString *page = collectNew_String();[m iString *parentDir = collectNewRange_String(dirName_Path(path));[m #if !defined (iPlatformMobile)[m [31m- appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n",[m [32m+[m[32m appendFormat_String(page, "=> %s " keyUpArrow_Icon " %s" iPathSeparator "\n\n",[m cstrCollect_String(makeFileUrl_String(parentDir)),[m cstr_String(parentDir));[m #endif[m [36m@@ -792,14 +792,14 @@[m [mvoid submit_GmRequest(iGmRequest *d) {[m if (!equal_Rangecc(parentDir, ".")) {[m /* A subdirectory. */[m appendFormat_String(page,[m [31m- "=> ../ " upArrow_Icon " %s" iPathSeparator[m [32m+[m[32m "=> ../ " keyUpArrow_Icon " %s" iPathSeparator[m "\n",[m cstr_Rangecc(parentDir));[m }[m else {[m /* Top-level directory. */[m appendFormat_String(page,[m [31m- "=> %s/ " upArrow_Icon " Root\n",[m [32m+[m[32m "=> %s/ " keyUpArrow_Icon " Root\n",[m cstr_String(containerUrl));[m }[m appendFormat_String(page, "# %s\n\n", cstr_Rangecc(baseName_Path(collectNewRange_String(curDir))));[m [1mdiff --git a/src/sitespec.c b/src/sitespec.c[m [1mindex 0db471d8..5f29e55f 100644[m [1m--- a/src/sitespec.c[m [1m+++ b/src/sitespec.c[m [36m@@ -75,6 +75,7 @@[m [mstruct Impl_SiteSpec {[m iString saveDir;[m iStringHash sites;[m iSiteParams *loadParams;[m [32m+[m[32m enum iImportMethod loadMethod;[m };[m [m static iSiteSpec siteSpec_;[m [36m@@ -127,7 +128,10 @@[m [mstatic void handleIniTable_SiteSpec_(void *context, const iString *table, iBool[m }[m else {[m iAssert(d->loadParams != NULL);[m [31m- insert_StringHash(&d->sites, table, d->loadParams);[m [32m+[m[32m if (d->loadMethod == all_ImportMethod ||[m [32m+[m[32m (d->loadMethod == ifMissing_ImportMethod && !contains_StringHash(&d->sites, table))) {[m [32m+[m[32m insert_StringHash(&d->sites, table, d->loadParams);[m [32m+[m[32m }[m iReleasePtr(&d->loadParams);[m }[m }[m [36m@@ -163,62 +167,74 @@[m [mstatic void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con[m }[m [m static iBool load_SiteSpec_(iSiteSpec *d) {[m [31m- iBool ok = iFalse;[m [32m+[m[32m iBool ok = iFalse;[m[41m [m iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, fileName_SiteSpec_)));[m if (open_File(f, readOnly_FileMode | text_FileMode)) {[m [31m- iTomlParser *toml = new_TomlParser();[m [31m- setHandlers_TomlParser(toml, handleIniTable_SiteSpec_, handleIniKeyValue_SiteSpec_, d);[m [31m- ok = parse_TomlParser(toml, collect_String(readString_File(f)));[m [31m- delete_TomlParser(toml);[m [32m+[m[32m ok = deserialize_SiteSpec(stream_File(f), all_ImportMethod);[m }[m iRelease(f);[m iAssert(d->loadParams == NULL);[m return ok;[m }[m [m [32m+[m[32miBool deserialize_SiteSpec(iStream *ins, enum iImportMethod loadMethod) {[m [32m+[m[32m iSiteSpec *d = &siteSpec_;[m [32m+[m[32m d->loadMethod = loadMethod;[m [32m+[m[32m iTomlParser *toml = new_TomlParser();[m [32m+[m[32m setHandlers_TomlParser(toml, handleIniTable_SiteSpec_, handleIniKeyValue_SiteSpec_, d);[m [32m+[m[32m iBool ok = parse_TomlParser(toml, collect_String(readString_Stream(ins)));[m [32m+[m[32m delete_TomlParser(toml);[m [32m+[m[32m return ok;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid serialize_SiteSpec(iStream *out) {[m [32m+[m[32m iSiteSpec *d = &siteSpec_;[m [32m+[m[32m iString *buf = new_String();[m [32m+[m[32m iConstForEach(StringHash, i, &d->sites) {[m [32m+[m[32m iBeginCollect();[m [32m+[m[32m const iBlock * key = &i.value->keyBlock;[m [32m+[m[32m const iSiteParams *params = i.value->object;[m [32m+[m[32m clear_String(buf);[m [32m+[m[32m if (params->titanPort) {[m [32m+[m[32m appendFormat_String(buf, "titanPort = %u\n", params->titanPort);[m [32m+[m[32m }[m [32m+[m[32m if (!isEmpty_String(¶ms->titanIdentity)) {[m [32m+[m[32m appendFormat_String([m [32m+[m[32m buf, "titanIdentity = \"%s\"\n", cstr_String(¶ms->titanIdentity));[m [32m+[m[32m }[m [32m+[m[32m if (params->dismissWarnings) {[m [32m+[m[32m appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings);[m [32m+[m[32m }[m [32m+[m[32m if (!isEmpty_StringArray(¶ms->usedIdentities)) {[m [32m+[m[32m appendFormat_String([m [32m+[m[32m buf,[m [32m+[m[32m "usedIdentities = \"%s\"\n",[m [32m+[m[32m cstrCollect_String(joinCStr_StringArray(¶ms->usedIdentities, " ")));[m [32m+[m[32m }[m [32m+[m[32m if (!isEmpty_String(¶ms->paletteSeed)) {[m [32m+[m[32m appendCStr_String(buf, "paletteSeed = \"");[m [32m+[m[32m append_String(buf, collect_String(quote_String(¶ms->paletteSeed, iFalse)));[m [32m+[m[32m appendCStr_String(buf, "\"\n");[m [32m+[m[32m }[m [32m+[m[32m if (!params->tlsSessionCache) {[m [32m+[m[32m appendCStr_String(buf, "tlsSessionCache = false\n");[m [32m+[m[32m }[m [32m+[m[32m if (!isEmpty_String(buf)) {[m [32m+[m[32m writeData_Stream(out, "[", 1);[m [32m+[m[32m writeData_Stream(out, constData_Block(key), size_Block(key));[m [32m+[m[32m writeData_Stream(out, "]\n", 2);[m [32m+[m[32m appendCStr_String(buf, "\n");[m [32m+[m[32m write_Stream(out, utf8_String(buf));[m [32m+[m[32m }[m [32m+[m[32m iEndCollect();[m [32m+[m[32m }[m [32m+[m[32m delete_String(buf);[m[41m [m [32m+[m[32m}[m [32m+[m static void save_SiteSpec_(iSiteSpec *d) {[m iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, fileName_SiteSpec_)));[m if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [31m- iString *buf = new_String();[m [31m- iConstForEach(StringHash, i, &d->sites) {[m [31m- iBeginCollect();[m [31m- const iBlock * key = &i.value->keyBlock;[m [31m- const iSiteParams *params = i.value->object;[m [31m- clear_String(buf);[m [31m- if (params->titanPort) {[m [31m- appendFormat_String(buf, "titanPort = %u\n", params->titanPort);[m [31m- }[m [31m- if (!isEmpty_String(¶ms->titanIdentity)) {[m [31m- appendFormat_String([m [31m- buf, "titanIdentity = \"%s\"\n", cstr_String(¶ms->titanIdentity));[m [31m- }[m [31m- if (params->dismissWarnings) {[m [31m- appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings);[m [31m- }[m [31m- if (!isEmpty_StringArray(¶ms->usedIdentities)) {[m [31m- appendFormat_String([m [31m- buf,[m [31m- "usedIdentities = \"%s\"\n",[m [31m- cstrCollect_String(joinCStr_StringArray(¶ms->usedIdentities, " ")));[m [31m- }[m [31m- if (!isEmpty_String(¶ms->paletteSeed)) {[m [31m- appendCStr_String(buf, "paletteSeed = \"");[m [31m- append_String(buf, collect_String(quote_String(¶ms->paletteSeed, iFalse)));[m [31m- appendCStr_String(buf, "\"\n");[m [31m- }[m [31m- if (!params->tlsSessionCache) {[m [31m- appendCStr_String(buf, "tlsSessionCache = false\n");[m [31m- }[m [31m- if (!isEmpty_String(buf)) {[m [31m- writeData_File(f, "[", 1);[m [31m- writeData_File(f, constData_Block(key), size_Block(key));[m [31m- writeData_File(f, "]\n", 2);[m [31m- appendCStr_String(buf, "\n");[m [31m- write_File(f, utf8_String(buf));[m [31m- }[m [31m- iEndCollect();[m [31m- }[m [31m- delete_String(buf);[m [32m+[m[32m serialize_SiteSpec(stream_File(f));[m }[m iRelease(f);[m }[m [1mdiff --git a/src/sitespec.h b/src/sitespec.h[m [1mindex 15219c62..a12bdb00 100644[m [1m--- a/src/sitespec.h[m [1m+++ b/src/sitespec.h[m [36m@@ -22,6 +22,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #pragma once[m [m [32m+[m[32m#include "defs.h"[m #include [m [m iDeclareType(SiteSpec)[m [36m@@ -38,6 +39,9 @@[m [menum iSiteSpecKey {[m void init_SiteSpec (const char *saveDir);[m void deinit_SiteSpec (void);[m [m [32m+[m[32mvoid serialize_SiteSpec (iStream *);[m [32m+[m[32miBool deserialize_SiteSpec (iStream *, enum iImportMethod);[m [32m+[m /* changes saved immediately */[m void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); [m void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value);[m [1mdiff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c[m [1mindex d6c63a30..14fd2433 100644[m [1m--- a/src/ui/documentwidget.c[m [1m+++ b/src/ui/documentwidget.c[m [36m@@ -31,6 +31,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include "bookmarks.h"[m #include "command.h"[m #include "defs.h"[m [32m+[m[32m#include "export.h"[m #include "gempub.h"[m #include "gmcerts.h"[m #include "gmdocument.h"[m [36m@@ -2415,6 +2416,9 @@[m [mstatic const char *zipPageHeading_(const iRangecc mime) {[m else if (equalCase_Rangecc(mime, mimeType_FontPack)) {[m return fontpack_Icon " Fontpack";[m }[m [32m+[m[32m else if (equalCase_Rangecc(mime, mimeType_Export)) {[m [32m+[m[32m return package_Icon " ${heading.archive.userdata}";[m [32m+[m[32m }[m iRangecc type = iNullRange;[m nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */[m nextSplit_Rangecc(mime, "/", &type);[m [36m@@ -2701,13 +2705,15 @@[m [mstatic void updateDocument_DocumentWidget_(iDocumentWidget *d,[m (equal_Rangecc(param, "application/zip") ||[m (startsWith_Rangecc(param, "application/") &&[m endsWithCase_Rangecc(param, "+zip")))) {[m [32m+[m[32m iArray *footerItems = collectNew_Array(sizeof(iMenuItem));[m clear_String(&str);[m docFormat = gemini_SourceFormat;[m setRange_String(&d->sourceMime, param);[m [32m+[m[32m iArchive *zip = new_Archive();[m [32m+[m[32m openData_Archive(zip, &response->body);[m if (equal_Rangecc(param, mimeType_FontPack)) {[m /* Show some information about fontpacks, and set up footer actions. */[m [31m- iArchive *zip = iClob(new_Archive());[m [31m- if (openData_Archive(zip, &response->body)) {[m [32m+[m[32m if (isOpen_Archive(zip)) {[m iFontPack *fp = new_FontPack();[m setUrl_FontPack(fp, d->mod.url);[m setStandalone_FontPack(fp, iTrue);[m [36m@@ -2726,28 +2732,54 @@[m [mstatic void updateDocument_DocumentWidget_(iDocumentWidget *d,[m }[m }[m else {[m [31m- format_String(&str, "# %s\n", zipPageHeading_(param));[m [32m+[m[32m if (detect_Export(zip)) {[m [32m+[m[32m setCStr_String(&d->sourceMime, mimeType_Export);[m [32m+[m[32m pushBack_Array(footerItems,[m [32m+[m[32m &(iMenuItem){ openExt_Icon " ${menu.open.external}",[m [32m+[m[32m SDLK_RETURN,[m [32m+[m[32m KMOD_PRIMARY,[m [32m+[m[32m "document.save extview:1" });[m [32m+[m[32m }[m [32m+[m[32m format_String(&str, "# %s\n", zipPageHeading_(range_String(&d->sourceMime)));[m appendFormat_String(&str,[m cstr_Lang("doc.archive"),[m cstr_Rangecc(baseName_Path(d->mod.url)));[m appendCStr_String(&str, "\n");[m }[m [32m+[m[32m iRelease(zip);[m appendCStr_String(&str, "\n");[m iString *localPath = localFilePathFromUrl_String(d->mod.url);[m [31m- if (!localPath) {[m [32m+[m[32m if (!localPath || !fileExists_FileInfo(localPath)) {[m iString *key = collectNew_String();[m toString_Sym(SDLK_s, KMOD_PRIMARY, key);[m appendFormat_String(&str, "%s\n\n",[m format_CStr(cstr_Lang("error.unsupported.suggestsave"),[m cstr_String(key),[m saveToDownloads_Label));[m [32m+[m[32m pushBack_Array(footerItems,[m [32m+[m[32m &(iMenuItem){ translateCStr_Lang(download_Icon[m [32m+[m[32m " " saveToDownloads_Label),[m [32m+[m[32m 0,[m [32m+[m[32m 0,[m [32m+[m[32m "document.save" });[m }[m [31m- delete_String(localPath);[m [31m- if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {[m [31m- appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",[m [32m+[m[32m if (localPath && fileExists_FileInfo(localPath)) {[m [32m+[m[32m if (!cmp_String(&d->sourceMime, mimeType_Export)) {[m [32m+[m[32m pushFront_Array(footerItems,[m [32m+[m[32m &(iMenuItem){ import_Icon " ${menu.import}",[m [32m+[m[32m SDLK_RETURN,[m [32m+[m[32m 0,[m [32m+[m[32m format_CStr("!import path:%s",[m [32m+[m[32m cstr_String(localPath)) });[m [32m+[m[32m }[m [32m+[m[32m appendFormat_String(&str,[m [32m+[m[32m "=> %s/ " folder_Icon " ${doc.archive.view}\n",[m cstr_String(withSpacesEncoded_String(d->mod.url)));[m }[m [32m+[m[32m delete_String(localPath);[m translate_Lang(&str);[m [32m+[m[32m makeFooterButtons_DocumentWidget_([m [32m+[m[32m d, constData_Array(footerItems), size_Array(footerItems));[m }[m else if (startsWith_Rangecc(param, "image/") ||[m startsWith_Rangecc(param, "audio/")) {[m [36m@@ -4287,7 +4319,8 @@[m [mstatic iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)[m }[m else if (!isEmpty_Block(&d->sourceContent)) {[m if (argLabel_Command(cmd, "extview")) {[m [31m- if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {[m [32m+[m[32m if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file") &&[m [32m+[m[32m fileExists_FileInfo(collect_String(localFilePathFromUrl_String(d->mod.url)))) {[m /* Already a file so just open it directly. */[m postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url));[m }[m [1mdiff --git a/src/ui/util.c b/src/ui/util.c[m [1mindex c3e39821..608ee116 100644[m [1m--- a/src/ui/util.c[m [1m+++ b/src/ui/util.c[m [36m@@ -29,6 +29,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include "command.h"[m #include "defs.h"[m #include "documentwidget.h"[m [32m+[m[32m#include "export.h"[m #include "feeds.h"[m #include "gmutil.h"[m #include "inputwidget.h"[m [36m@@ -2658,7 +2659,7 @@[m [miWidget *makePreferences_Widget(void) {[m const iMenuItem identityPanelItems[] = {[m { "title id:sidebar.identities" },[m { "certlist" },[m [31m- { "navi.action id:prefs.ident.import text:" inbox_Icon, 0, 0, "ident.import" },[m [32m+[m[32m { "navi.action id:prefs.ident.import text:" import_Icon, 0, 0, "ident.import" },[m { "navi.action id:prefs.ident.new text:" add_Icon, 0, 0, "ident.new" },[m { NULL } [m };[m [36m@@ -3718,6 +3719,118 @@[m [miWidget *makeGlyphFinder_Widget(void) {[m return dlg;[m }[m [m [32m+[m[32mstatic enum iImportMethod checkImportMethod_(const iWidget *dlg, const char *id) {[m [32m+[m[32m return isSelected_Widget(findChild_Widget(dlg, format_CStr("%s.0", id))) ? none_ImportMethod[m [32m+[m[32m : isSelected_Widget(findChild_Widget(dlg, format_CStr("%s.1", id)))[m [32m+[m[32m ? ifMissing_ImportMethod[m [32m+[m[32m : all_ImportMethod;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic iBool handleUserDataImporterCommands_(iWidget *dlg, const char *cmd) {[m [32m+[m[32m if (equalWidget_Command(cmd, dlg, "importer.cancel") ||[m [32m+[m[32m equalWidget_Command(cmd, dlg, "importer.accept")) {[m [32m+[m[32m if (equal_Command(cmd, "importer.accept")) {[m [32m+[m[32m /* Compose the final import command. */[m [32m+[m[32m enum iImportMethod bookmarkMethod = checkImportMethod_(dlg, "importer.bookmark");[m [32m+[m[32m enum iImportMethod identMethod = checkImportMethod_(dlg, "importer.idents");[m [32m+[m[32m enum iImportMethod trustedMethod = checkImportMethod_(dlg, "importer.trusted");[m [32m+[m[32m enum iImportMethod sitespecMethod = checkImportMethod_(dlg, "importer.sitespec");[m [32m+[m[32m enum iImportMethod visitedMethod =[m [32m+[m[32m isSelected_Widget(findChild_Widget(dlg, "importer.history")) ? all_ImportMethod[m [32m+[m[32m : none_ImportMethod;[m [32m+[m[32m postCommandf_App([m [32m+[m[32m "import arg:1 bookmarks:%d idents:%d trusted:%d visited:%d sitespec:%d path:%s",[m [32m+[m[32m bookmarkMethod,[m [32m+[m[32m identMethod,[m [32m+[m[32m trustedMethod,[m [32m+[m[32m visitedMethod,[m [32m+[m[32m sitespecMethod,[m [32m+[m[32m suffixPtr_Command(cmd, "path"));[m [32m+[m[32m }[m [32m+[m[32m setupSheetTransition_Mobile(dlg, dialogTransitionDir_Widget(dlg));[m [32m+[m[32m destroy_Widget(dlg);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m else if (equalWidget_Command(cmd, dlg, "importer.selectall")) {[m [32m+[m[32m postCommand_Widget(findChild_Widget(dlg, "importer.bookmark.1"), "trigger");[m [32m+[m[32m postCommand_Widget(findChild_Widget(dlg, "importer.trusted.1"), "trigger");[m [32m+[m[32m postCommand_Widget(findChild_Widget(dlg, "importer.idents.1"), "trigger");[m [32m+[m[32m postCommand_Widget(findChild_Widget(dlg, "importer.sitespec.1"), "trigger");[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "importer.history"), iTrue);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m return iFalse;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miWidget *makeUserDataImporter_Dialog(const iString *archivePath) {[m [32m+[m[32m iWidget *dlg;[m [32m+[m[32m const iMenuItem actions[] = {[m [32m+[m[32m { "${menu.selectall}", 0, 0, "importer.selectall" },[m [32m+[m[32m { "---" },[m [32m+[m[32m { "${cancel}", SDLK_ESCAPE, 0, "importer.cancel" },[m [32m+[m[32m { uiTextAction_ColorEscape "${import.userdata}",[m [32m+[m[32m SDLK_RETURN, KMOD_PRIMARY,[m [32m+[m[32m format_CStr("importer.accept path:%s", cstr_String(archivePath)) },[m [32m+[m[32m };[m [32m+[m[32m if (isUsingPanelLayout_Mobile()) {[m [32m+[m[32m dlg = makePanels_Mobile("importer", (iMenuItem[]){[m [32m+[m[32m { NULL }[m [32m+[m[32m }, actions, iElemCount(actions));[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m dlg = makeSheet_Widget("importer");[m [32m+[m[32m addDialogTitle_(dlg, "${heading.import.userdata}", NULL);[m [32m+[m[32m iWidget *headings, *values;[m [32m+[m[32m addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));[m [32m+[m[32m /* Bookmarks. */[m [32m+[m[32m addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.bookmarks}")));[m [32m+[m[32m iWidget *radio = new_Widget(); {[m [32m+[m[32m addRadioButton_(radio, "importer.bookmark.0", "${dlg.userdata.no}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.bookmark.1", "${dlg.userdata.missing}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.bookmark.2", "${dlg.userdata.alldup}", ".");[m [32m+[m[32m }[m [32m+[m[32m addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);[m [32m+[m[32m /* Site-specific. */[m [32m+[m[32m addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.sitespec}")));[m [32m+[m[32m radio = new_Widget(); {[m [32m+[m[32m addRadioButton_(radio, "importer.sitespec.0", "${dlg.userdata.no}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.sitespec.1", "${dlg.userdata.missing}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.sitespec.2", "${dlg.userdata.all}", ".");[m [32m+[m[32m }[m [32m+[m[32m addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);[m [32m+[m[32m /* Trusted certs. */[m [32m+[m[32m addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.trusted}")));[m [32m+[m[32m radio = new_Widget(); {[m [32m+[m[32m addRadioButton_(radio, "importer.trusted.0", "${dlg.userdata.no}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.trusted.1", "${dlg.userdata.missing}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.trusted.2", "${dlg.userdata.all}", ".");[m [32m+[m[32m }[m [32m+[m[32m addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);[m [32m+[m[32m /* Identities. */[m [32m+[m[32m addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.idents}")));[m [32m+[m[32m radio = new_Widget(); {[m [32m+[m[32m addRadioButton_(radio, "importer.idents.0", "${dlg.userdata.no}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.idents.1", "${dlg.userdata.missing}", ".");[m [32m+[m[32m addRadioButton_(radio, "importer.idents.2", "${dlg.userdata.all}", ".");[m [32m+[m[32m }[m [32m+[m[32m addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);[m [32m+[m[32m addDialogToggle_(headings, values, "${import.userdata.history}", "importer.history");[m [32m+[m[32m addDialogPadding_(headings, values);[m [32m+[m[32m addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));[m [32m+[m[32m addChild_Widget(dlg->root->widget, iClob(dlg));[m [32m+[m[32m arrange_Widget(dlg);[m [32m+[m[32m arrange_Widget(dlg);[m [32m+[m[32m setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | dialogTransitionDir_Widget(dlg));[m [32m+[m[32m }[m [32m+[m[32m /* Initialize. */[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "importer.bookmark.0"), iTrue);[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "importer.idents.0"), iTrue);[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "importer.sitespec.0"), iTrue);[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "importer.trusted.0"), iTrue);[m [32m+[m[32m setCommandHandler_Widget(dlg, handleUserDataImporterCommands_);[m [32m+[m[32m return dlg;[m [32m+[m[32m}[m [32m+[m /*----------------------------------------------------------------------------------------------*/[m [m void init_PerfTimer(iPerfTimer *d) {[m [1mdiff --git a/src/ui/util.h b/src/ui/util.h[m [1mindex 50440137..cda1af61 100644[m [1m--- a/src/ui/util.h[m [1m+++ b/src/ui/util.h[m [36m@@ -346,6 +346,7 @@[m [miWidget * makeFeedSettings_Widget (uint32_t bookmarkId);[m iWidget * makeSiteSpecificSettings_Widget (const iString *url);[m iWidget * makeTranslation_Widget (iWidget *parent);[m iWidget * makeGlyphFinder_Widget (void);[m [32m+[m[32miWidget * makeUserDataImporter_Dialog (const iString *archivePath);[m [m const char * languageId_String (const iString *menuItemLabel);[m int languageIndex_CStr (const char *langId);[m [1mdiff --git a/src/ui/window.c b/src/ui/window.c[m [1mindex f58d7b46..c981e7cf 100644[m [1m--- a/src/ui/window.c[m [1m+++ b/src/ui/window.c[m [36m@@ -87,6 +87,7 @@[m [mstatic const iMenuItem fileMenuItems_[] = {[m { saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },[m { "---", 0, 0, NULL },[m { "${menu.downloads}", 0, 0, "downloads.open" },[m [32m+[m[32m { "${menu.export}", 0, 0, "export" },[m };[m [m static const iMenuItem editMenuItems_[] = {[m [1mdiff --git a/src/visited.c b/src/visited.c[m [1mindex f6299ddb..c1c091e2 100644[m [1m--- a/src/visited.c[m [1m+++ b/src/visited.c[m [36m@@ -72,53 +72,71 @@[m [mvoid deinit_Visited(iVisited *d) {[m delete_Mutex(d->mtx);[m }[m [m [31m-void save_Visited(const iVisited *d, const char *dirPath) {[m [32m+[m[32mvoid serialize_Visited(const iVisited *d, iStream *out) {[m iString *line = new_String();[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m iConstForEach(Array, i, &d->visited.values) {[m [32m+[m[32m const iVisitedUrl *item = i.value;[m [32m+[m[32m format_String(line,[m [32m+[m[32m "%llu %04x %s\n",[m [32m+[m[32m (unsigned long long) integralSeconds_Time(&item->when),[m [32m+[m[32m item->flags,[m [32m+[m[32m cstr_String(&item->url));[m [32m+[m[32m writeData_Stream(out, cstr_String(line), size_String(line));[m [32m+[m[32m }[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m delete_String(line);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid save_Visited(const iVisited *d, const char *dirPath) {[m iFile *f = newCStr_File(concatPath_CStr(dirPath, "visited.2.txt"));[m if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [31m- lock_Mutex(d->mtx);[m [31m- iConstForEach(Array, i, &d->visited.values) {[m [31m- const iVisitedUrl *item = i.value;[m [31m- format_String(line,[m [31m- "%llu %04x %s\n",[m [31m- (unsigned long long) integralSeconds_Time(&item->when),[m [31m- item->flags,[m [31m- cstr_String(&item->url));[m [31m- writeData_File(f, cstr_String(line), size_String(line));[m [31m- }[m [31m- unlock_Mutex(d->mtx);[m [32m+[m[32m serialize_Visited(d, stream_File(f));[m }[m iRelease(f);[m [31m- delete_String(line);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid deserialize_Visited(iVisited *d, iStream *ins, iBool mergeKeepingLatest) {[m [32m+[m[32m const iRangecc src = range_Block(collect_Block(readAll_Stream(ins)));[m [32m+[m[32m iRangecc line = iNullRange;[m [32m+[m[32m iTime now;[m [32m+[m[32m initCurrent_Time(&now);[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m while (nextSplit_Rangecc(src, "\n", &line)) {[m [32m+[m[32m if (size_Range(&line) < 8) continue;[m [32m+[m[32m char *endp = NULL;[m [32m+[m[32m const unsigned long long ts = strtoull(line.start, &endp, 10);[m [32m+[m[32m if (ts == 0) break;[m [32m+[m[32m const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16);[m [32m+[m[32m const char *urlStart = skipSpace_CStr(endp);[m [32m+[m[32m iVisitedUrl item;[m [32m+[m[32m item.when.ts = (struct timespec){ .tv_sec = ts };[m [32m+[m[32m if (~flags & kept_VisitedUrlFlag &&[m [32m+[m[32m secondsSince_Time(&now, &item.when) > maxAge_Visited) {[m [32m+[m[32m continue; /* Too old. */[m [32m+[m[32m }[m [32m+[m[32m item.flags = flags;[m [32m+[m[32m initRange_String(&item.url, (iRangecc){ urlStart, line.end });[m [32m+[m[32m set_String(&item.url, &item.url);[m [32m+[m[32m if (mergeKeepingLatest) {[m [32m+[m[32m /* Check if we already have this. */[m [32m+[m[32m size_t existingPos;[m [32m+[m[32m if (locate_SortedArray(&d->visited, &item, &existingPos)) {[m [32m+[m[32m iVisitedUrl *existing = at_SortedArray(&d->visited, existingPos);[m [32m+[m[32m max_Time(&existing->when, &item.when);[m [32m+[m[32m existing->flags = item.flags;[m [32m+[m[32m continue;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m insert_SortedArray(&d->visited, &item);[m [32m+[m[32m }[m [32m+[m[32m unlock_Mutex(d->mtx);[m }[m [m void load_Visited(iVisited *d, const char *dirPath) {[m iFile *f = newCStr_File(concatPath_CStr(dirPath, "visited.2.txt"));[m if (open_File(f, readOnly_FileMode | text_FileMode)) {[m [31m- lock_Mutex(d->mtx);[m [31m- const iRangecc src = range_Block(collect_Block(readAll_File(f)));[m [31m- iRangecc line = iNullRange;[m [31m- iTime now;[m [31m- initCurrent_Time(&now);[m [31m- while (nextSplit_Rangecc(src, "\n", &line)) {[m [31m- if (size_Range(&line) < 8) continue;[m [31m- char *endp = NULL;[m [31m- const unsigned long long ts = strtoull(line.start, &endp, 10);[m [31m- if (ts == 0) break;[m [31m- const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16);[m [31m- const char *urlStart = skipSpace_CStr(endp);[m [31m- iVisitedUrl item;[m [31m- item.when.ts = (struct timespec){ .tv_sec = ts };[m [31m- if (~flags & kept_VisitedUrlFlag &&[m [31m- secondsSince_Time(&now, &item.when) > maxAge_Visited) {[m [31m- continue; /* Too old. */[m [31m- }[m [31m- item.flags = flags;[m [31m- initRange_String(&item.url, (iRangecc){ urlStart, line.end });[m [31m- set_String(&item.url, &item.url);[m [31m- insert_SortedArray(&d->visited, &item);[m [31m- }[m [31m- unlock_Mutex(d->mtx);[m [32m+[m[32m deserialize_Visited(d, stream_File(f), iFalse /* no merge */);[m }[m iRelease(f);[m }[m [1mdiff --git a/src/visited.h b/src/visited.h[m [1mindex 0fde1d1f..1484492e 100644[m [1m--- a/src/visited.h[m [1m+++ b/src/visited.h[m [36m@@ -50,6 +50,8 @@[m [miDeclareTypeConstruction(Visited)[m void clear_Visited (iVisited *);[m void load_Visited (iVisited *, const char *dirPath);[m void save_Visited (const iVisited *, const char *dirPath);[m [32m+[m[32mvoid serialize_Visited (const iVisited *, iStream *out);[m [32m+[m[32mvoid deserialize_Visited (iVisited *, iStream *ins, iBool mergeKeepingLatest);[m [m iTime urlVisitTime_Visited (const iVisited *, const iString *url);[m void visitUrl_Visited (iVisited *, const iString *url, uint16_t visitFlags); /* adds URL to the visited URLs set */[m
text/gemini; charset=utf-8
This content has been proxied by September (ba2dc).