[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 <the_Foundation/buffer.h>[m
#include <the_Foundation/commandline.h>[m
#include <the_Foundation/file.h>[m
#include <the_Foundation/fileinfo.h>[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 <the_Foundation/hash.h>[m
#include <the_Foundation/ptrarray.h>[m
#include <the_Foundation/string.h>[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 jaakko.keranen@iki.fi[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 <the_Foundation/buffer.h>[m
[32m+[m[32m#include <the_Foundation/file.h>[m
[32m+[m[32m#include <the_Foundation/fileinfo.h>[m
[32m+[m[32m#include <the_Foundation/path.h>[m
[32m+[m[32m#include <the_Foundation/time.h>[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 jaakko.keranen@iki.fi[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 <the_Foundation/archive.h>[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 <the_Foundation/time.h>[m
#include <ctype.h>[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 <the_Foundation/ptrarray.h>[m
#include <the_Foundation/stringset.h>[m
#include <the_Foundation/tlsrequest.h>[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 <the_Foundation/stringarray.h>[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/plain
This content has been proxied by September (3851b).