diff --git a/CMakeLists.txt b/CMakeLists.txt

index bcf1c7bf..69ac3df0 100644

--- a/CMakeLists.txt

+++ b/CMakeLists.txt

@@ -150,6 +150,8 @@ set (SOURCES

 src/bookmarks.c

 src/bookmarks.h

 src/defs.h

+ src/export.c

+ src/export.h

 src/feeds.c

 src/feeds.h

 src/fontpack.c

diff --git a/lib/the_Foundation b/lib/the_Foundation

index 05fe3fdb..32e91939 160000

--- a/lib/the_Foundation

+++ b/lib/the_Foundation

@@ -1 +1 @@

-Subproject commit 05fe3fdb17ebf64f1830dc7bbe3359e5f062b1f0

+Subproject commit 32e91939443d6e831eb438a81b6a8bc5e17ac021

diff --git a/po/en.po b/po/en.po

index 699fabb9..f8cc0caa 100644

--- a/po/en.po

+++ b/po/en.po

@@ -9,6 +9,9 @@ msgstr "Loading"

msgid "doc.archive"

msgstr "%s is a compressed archive."



+msgid "heading.archive.userdata"

+msgstr "User Data Archive"

+

msgid "doc.archive.view"

msgstr "View archive contents"



@@ -181,6 +184,12 @@ msgstr "Open Location…"

msgid "menu.downloads"

msgstr "Show Downloads"



+msgid "menu.export"

+msgstr "Export User Data"

+

+msgid "menu.import"

+msgstr "Import User Data…"

+

msgid "menu.pageinfo"

msgstr "Show Page Information"



@@ -971,6 +980,9 @@ msgstr "Import Certificate/Key File"

msgid "dlg.certimport.import"

msgstr "Import"



+msgid "dlg.certimport.paste"

+msgstr "Paste from Clipboard"

+

msgid "dlg.certimport.notes"

msgstr "Notes:"



@@ -989,6 +1001,48 @@ msgstr "Audio"

msgid "link.hint.image"

msgstr "Image"



+msgid "heading.import.userdata"

+msgstr "Import User Data"

+

+msgid "heading.import.userdata.error"

+msgstr "Import Failed"

+

+msgid "import.userdata.bookmarks"

+msgstr "Bookmarks:"

+

+msgid "import.userdata.idents"

+msgstr "Identities:"

+

+msgid "import.userdata.history"

+msgstr "History:"

+

+msgid "import.userdata.trusted"

+msgstr "Trusted certificates:"

+

+msgid "import.userdata.sitespec"

+msgstr "Site settings:"

+

+msgid "dlg.userdata.no"

+msgstr "None"

+

+msgid "dlg.userdata.missing"

+msgstr "If Missing"

+

+msgid "dlg.userdata.all"

+msgstr "All"

+

+msgid "dlg.userdata.alldup"

+msgstr "All (Keep Duplicates)"

+

+msgid "import.userdata"

+msgstr "Import Selected Data"

+

+msgid "import.userdata.dupfolder"

+msgstr "Imported Duplicates"

+

+msgid "import.userdata.error"

+msgstr "%s is not a valid Lagrange export archive."

+

msgid "bookmark.title.blank"

msgstr "Blank Page"



diff --git a/res/lang/cs.bin b/res/lang/cs.bin

index e70b4c84..d2889c1a 100644

Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ

diff --git a/res/lang/de.bin b/res/lang/de.bin

index a96ffb17..cc4b305e 100644

Binary files a/res/lang/de.bin and b/res/lang/de.bin differ

diff --git a/res/lang/en.bin b/res/lang/en.bin

index c7ce6d11..b1801eec 100644

Binary files a/res/lang/en.bin and b/res/lang/en.bin differ

diff --git a/res/lang/eo.bin b/res/lang/eo.bin

index bdb009e9..155ef35c 100644

Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ

diff --git a/res/lang/es.bin b/res/lang/es.bin

index f20d84da..3f8b7a61 100644

Binary files a/res/lang/es.bin and b/res/lang/es.bin differ

diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin

index 8770ba16..b19bfa09 100644

Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ

diff --git a/res/lang/fi.bin b/res/lang/fi.bin

index 28f7f22e..b7c7acb8 100644

Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ

diff --git a/res/lang/fr.bin b/res/lang/fr.bin

index 97a48bb2..28eb98ae 100644

Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ

diff --git a/res/lang/gl.bin b/res/lang/gl.bin

index 274bcf2b..e79c0db3 100644

Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ

diff --git a/res/lang/hu.bin b/res/lang/hu.bin

index b2bbc538..c72f74ad 100644

Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ

diff --git a/res/lang/ia.bin b/res/lang/ia.bin

index 9aa98b70..0527016f 100644

Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ

diff --git a/res/lang/ie.bin b/res/lang/ie.bin

index 317b9ddb..85cfd1a9 100644

Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ

diff --git a/res/lang/isv.bin b/res/lang/isv.bin

index 343d17e4..8e7c8169 100644

Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ

diff --git a/res/lang/it.bin b/res/lang/it.bin

index ef145008..e1296993 100644

Binary files a/res/lang/it.bin and b/res/lang/it.bin differ

diff --git a/res/lang/nl.bin b/res/lang/nl.bin

index 7bec0514..85470de6 100644

Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ

diff --git a/res/lang/pl.bin b/res/lang/pl.bin

index 171356c7..ec8e61da 100644

Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ

diff --git a/res/lang/ru.bin b/res/lang/ru.bin

index 92169af5..1467d74c 100644

Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ

diff --git a/res/lang/sk.bin b/res/lang/sk.bin

index 4d197933..b92fdc80 100644

Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ

diff --git a/res/lang/sr.bin b/res/lang/sr.bin

index d733cbef..75e3ac79 100644

Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ

diff --git a/res/lang/tok.bin b/res/lang/tok.bin

index 6ac6ac9f..f501f91c 100644

Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ

diff --git a/res/lang/tr.bin b/res/lang/tr.bin

index 0dc81e68..7ac94fc4 100644

Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ

diff --git a/res/lang/uk.bin b/res/lang/uk.bin

index 4ada5dfe..82e8208a 100644

Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ

diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin

index 818445b6..4697c37e 100644

Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ

diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin

index 17d95f10..54050a83 100644

Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ

diff --git a/src/app.c b/src/app.c

index e2802644..e317e4c6 100644

--- a/src/app.c

+++ b/src/app.c

@@ -23,17 +23,17 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "app.h"

#include "bookmarks.h"

#include "defs.h"

-#include "resources.h"

+#include "export.h"

#include "feeds.h"

-#include "mimehooks.h"

#include "gmcerts.h"

#include "gmdocument.h"

#include "gmutil.h"

#include "history.h"

#include "ipc.h"

+#include "mimehooks.h"

#include "periodic.h"

+#include "resources.h"

#include "sitespec.h"

-#include "updater.h"

#include "ui/certimportwidget.h"

#include "ui/color.h"

#include "ui/command.h"

@@ -43,13 +43,15 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "ui/labelwidget.h"

#include "ui/root.h"

#include "ui/sidebarwidget.h"

-#include "ui/touch.h"

#include "ui/text.h"

+#include "ui/touch.h"

#include "ui/uploadwidget.h"

#include "ui/util.h"

#include "ui/window.h"

+#include "updater.h"

#include "visited.h"



+#include <the_Foundation/buffer.h>

#include <the_Foundation/commandline.h>

#include <the_Foundation/file.h>

#include <the_Foundation/fileinfo.h>

@@ -3744,6 +3746,53 @@ iBool handleCommand_App(const char *cmd) {

     }

     return iTrue;

 }

+ else if (equal_Command(cmd, "export")) {

+ iExport *export = new_Export();

+ iBuffer *zip = new_Buffer();

+ generate_Export(export);

+ openEmpty_Buffer(zip);

+ serialize_Archive(archive_Export(export), stream_Buffer(zip));

+ iDocumentWidget *expTab = newTab_App(NULL, iTrue);

+ iDate now;

+ initCurrent_Date(&now);

+ setUrlAndSource_DocumentWidget(

+ expTab,

+ collect_String(format_Date(&now, "file:Lagrange_User_Data_%Y-%m-%d_%H%M%S.zip")),

+ collectNewCStr_String("application/zip"),

+ data_Buffer(zip));

+ iRelease(zip);

+ delete_Export(export);

+ return iTrue;

+ }

+ else if (equal_Command(cmd, "import")) {

+ const iString *path = collect_String(suffix_Command(cmd, "path"));

+ iArchive *zip = iClob(new_Archive());

+ if (openFile_Archive(zip, path)) {

+ if (!arg_Command(cmd)) {

+ makeUserDataImporter_Dialog(path);

+ return iTrue;

+ }

+ const int bookmarks = argLabel_Command(cmd, "bookmarks");

+ const int trusted = argLabel_Command(cmd, "trusted");

+ const int idents = argLabel_Command(cmd, "idents");

+ const int visited = argLabel_Command(cmd, "visited");

+ const int siteSpec = argLabel_Command(cmd, "sitespec");

+ iExport *export = new_Export();

+ if (load_Export(export, zip)) {

+ import_Export(export, bookmarks, idents, trusted, visited, siteSpec);

+ }

+ else {

+ makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.userdata.error}",

+ format_Lang("${import.userdata.error}", cstr_String(path))); 

+ }

+ delete_Export(export);

+ }

+ else {

+ makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.userdata.error}",

+ format_Lang("${import.userdata.error}", cstr_String(path)));

+ }

+ return iTrue;

+ }

#if defined (LAGRANGE_ENABLE_IPC)

 else if (equal_Command(cmd, "ipc.list.urls")) {

     iProcessId pid = argLabel_Command(cmd, "pid");

diff --git a/src/bookmarks.c b/src/bookmarks.c

index 500caa38..6f4b13ca 100644

--- a/src/bookmarks.c

+++ b/src/bookmarks.c

@@ -277,21 +277,43 @@ static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) {

iDeclareType(BookmarkLoader)

 

struct Impl_BookmarkLoader {

- iTomlParser *toml;

- iBookmarks * bookmarks;

- iBookmark * bm;

+ iTomlParser *toml;

+ iBookmarks *bookmarks;

+ iBookmark *bm;

+ uint32_t loadId;

+ enum iImportMethod method;

+ uint32_t baseId;

+ uint32_t dupFolderId;

+ iBool didImportDuplicates;

};



static void handleTable_BookmarkLoader_(void *context, const iString *table, iBool isStart) {

 iBookmarkLoader *d = context;

 if (isStart) {

     iAssert(!d->bm);

+ iAssert(d->method != none_ImportMethod);

     d->bm = new_Bookmark();

- const int id = toInt_String(table);

- d->bookmarks->idEnum = iMax(d->bookmarks->idEnum, id);

- insertId_Bookmarks_(d->bookmarks, d->bm, id);

- }

- else {

+ d->loadId = toInt_String(table) + d->baseId;

+ }

+ else if (d->bm) {

+ /* Check if import rules. */

+ if (d->baseId && !isFolder_Bookmark(d->bm)) {

+ const uint32_t existing = findUrl_Bookmarks(d->bookmarks, &d->bm->url);

+ if (existing) {

+ if (d->method == ifMissing_ImportMethod) {

+ /* Already have this one. */

+ delete_Bookmark(d->bm);

+ d->bm = NULL;

+ return;

+ }

+ else {

+ d->bm->parentId = d->dupFolderId;

+ d->didImportDuplicates = iTrue;

+ }

+ }

+ }

+ d->bookmarks->idEnum = iMax(d->bookmarks->idEnum, d->loadId);

+ insertId_Bookmarks_(d->bookmarks, d->bm, d->loadId);

     d->bm = NULL;

 }

}

@@ -319,7 +341,7 @@ static void handleKeyValue_BookmarkLoader_(void *context, const iString *table,

         initSeconds_Time(&bm->when, tv->value.int64);

     }

     else if (!cmp_String(key, "parent") && tv->type == int64_TomlType) {

- bm->parentId = tv->value.int64;

+ bm->parentId = tv->value.int64 + d->baseId;

     }

     else if (!cmp_String(key, "order") && tv->type == int64_TomlType) {

         bm->order = tv->value.int64;

@@ -335,16 +357,29 @@ static void init_BookmarkLoader(iBookmarkLoader *d, iBookmarks *bookmarks) {

 setHandlers_TomlParser(d->toml, handleTable_BookmarkLoader_, handleKeyValue_BookmarkLoader_, d);

 d->bookmarks = bookmarks;

 d->bm = NULL;

+ d->loadId = 0;

+ d->method = all_ImportMethod;

+ d->baseId = bookmarks->idEnum; /* allows importing bookmarks without ID conflicts */

+ d->dupFolderId = 0;

+ d->didImportDuplicates = iFalse;

}



static void deinit_BookmarkLoader(iBookmarkLoader *d) {

 delete_TomlParser(d->toml);

}



-static void load_BookmarkLoader(iBookmarkLoader *d, iFile *file) {

- if (!parse_TomlParser(d->toml, collect_String(readString_File(file)))) {

- fprintf(stderr, "[Bookmarks] syntax error(s) in %s\n", cstr_String(path_File(file)));

- } 

+static void load_BookmarkLoader(iBookmarkLoader *d, iStream *stream) {

+ if (d->baseId && d->method == all_ImportMethod) {

+ /* Make a folder for possible duplicate bookmarks. */

+ d->dupFolderId =

+ add_Bookmarks(d->bookmarks, NULL, string_Lang("import.userdata.dupfolder"), NULL, 0);

+ }

+ if (!parse_TomlParser(d->toml, collect_String(readString_Stream(stream)))) {

+ fprintf(stderr, "[Bookmarks] syntax error in bookmarks.ini\n");

+ }

+ if (d->dupFolderId && !d->didImportDuplicates) {

+ remove_Bookmarks(d->bookmarks, d->dupFolderId);

+ }

}



iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)

@@ -364,6 +399,46 @@ void sort_Bookmarks(iBookmarks *d, uint32_t parentId, iBookmarksCompareFunc cmp)

 unlock_Mutex(d->mtx);

}



+static void mergeFolders_BookmarkLoader(iBookmarkLoader *d) {

+ if (!d->baseId) {

+ /* Only merge after importing. */

+ return;

+ }

+ iHash *hash = &d->bookmarks->bookmarks;

+ iForEach(Hash, i, hash) {

+ iBookmark *imported = (iBookmark *) i.value;

+ if (isFolder_Bookmark(imported) && id_Bookmark(imported) >= d->baseId) {

+ /* If there already is a folder with a matching name, merge this one into it. */

+ iForEach(Hash, j, hash) {

+ iBookmark *old = (iBookmark *) j.value;

+ if (isFolder_Bookmark(old) && id_Bookmark(old) < d->baseId &&

+ equal_String(&imported->title, &old->title)) {

+ iForEach(Hash, k, hash) {

+ iBookmark *bm = (iBookmark *) k.value;

+ if (bm->parentId == id_Bookmark(imported)) {

+ bm->parentId = id_Bookmark(old);

+ }

+ }

+ remove_HashIterator(&i);

+ delete_Bookmark(imported);

+ break;

+ }

+ }

+ }

+ }

+}

+

+void deserialize_Bookmarks(iBookmarks *d, iStream *ins, enum iImportMethod method) {

+ lock_Mutex(d->mtx);

+ iBookmarkLoader loader;

+ init_BookmarkLoader(&loader, d);

+ loader.method = method;

+ load_BookmarkLoader(&loader, ins);

+ mergeFolders_BookmarkLoader(&loader);

+ deinit_BookmarkLoader(&loader);

+ unlock_Mutex(d->mtx);

+}

+

void load_Bookmarks(iBookmarks *d, const char *dirPath) {

 clear_Bookmarks(d);

 /* Load new .ini bookmarks, if present. */

@@ -377,49 +452,53 @@ void load_Bookmarks(iBookmarks *d, const char *dirPath) {

 }

 iBookmarkLoader loader;

 init_BookmarkLoader(&loader, d);

- load_BookmarkLoader(&loader, f);

+ load_BookmarkLoader(&loader, stream_File(f));

 deinit_BookmarkLoader(&loader);

}



+void serialize_Bookmarks(const iBookmarks *d, iStream *out) {

+ iString *str = collectNew_String();

+ format_String(str, "recentfolder = %u\n\n", d->recentFolderId);

+ writeData_Stream(out, cstr_String(str), size_String(str));

+ iConstForEach(Hash, i, &d->bookmarks) {

+ const iBookmark *bm = (const iBookmark *) i.value;

+ if (bm->flags & remote_BookmarkFlag) {

+ /* Remote bookmarks are not saved. */

+ continue;

+ }

+ iBeginCollect();

+ const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm));

+ format_String(str,

+ "[%d]\n"

+ "url = "%s"\n"

+ "title = "%s"\n"

+ "tags = "%s"\n"

+ "icon = 0x%x\n"

+ "created = %.0f # %s\n",

+ id_Bookmark(bm),

+ cstrCollect_String(quote_String(&bm->url, iFalse)),

+ cstrCollect_String(quote_String(&bm->title, iFalse)),

+ cstrCollect_String(quote_String(packedTags, iFalse)),

+ bm->icon,

+ seconds_Time(&bm->when),

+ cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));

+ if (bm->parentId) {

+ appendFormat_String(str, "parent = %d\n", bm->parentId);

+ }

+ if (bm->order) {

+ appendFormat_String(str, "order = %d\n", bm->order);

+ }

+ appendCStr_String(str, "\n");

+ writeData_Stream(out, cstr_String(str), size_String(str));

+ iEndCollect();

+ }

+}

+

void save_Bookmarks(const iBookmarks *d, const char *dirPath) {

 lock_Mutex(d->mtx);

 iFile *f = newCStr_File(concatPath_CStr(dirPath, fileName_Bookmarks_));

- if (open_File(f, writeOnly_FileMode | text_FileMode)) { 

- iString *str = collectNew_String();

- format_String(str, "recentfolder = %u\n\n", d->recentFolderId);

- writeData_File(f, cstr_String(str), size_String(str));

- iConstForEach(Hash, i, &d->bookmarks) {

- const iBookmark *bm = (const iBookmark *) i.value;

- if (bm->flags & remote_BookmarkFlag) {

- /* Remote bookmarks are not saved. */

- continue;

- }

- iBeginCollect();

- const iString *packedTags = collect_String(packedDotTags_Bookmark_(bm));

- format_String(str,

- "[%d]\n"

- "url = "%s"\n"

- "title = "%s"\n"

- "tags = "%s"\n"

- "icon = 0x%x\n"

- "created = %.0f # %s\n",

- id_Bookmark(bm),

- cstrCollect_String(quote_String(&bm->url, iFalse)),

- cstrCollect_String(quote_String(&bm->title, iFalse)),

- cstrCollect_String(quote_String(packedTags, iFalse)),

- bm->icon,

- seconds_Time(&bm->when),

- cstrCollect_String(format_Time(&bm->when, "%Y-%m-%d")));

- if (bm->parentId) {

- appendFormat_String(str, "parent = %d\n", bm->parentId);

- }

- if (bm->order) {

- appendFormat_String(str, "order = %d\n", bm->order);

- }

- appendCStr_String(str, "\n");

- writeData_File(f, cstr_String(str), size_String(str));

- iEndCollect();

- } 

+ if (open_File(f, writeOnly_FileMode | text_FileMode)) {

+ serialize_Bookmarks(d, stream_File(f));

 }

 iRelease(f);

 unlock_Mutex(d->mtx);

diff --git a/src/bookmarks.h b/src/bookmarks.h

index 08afdd8b..13a93748 100644

--- a/src/bookmarks.h

+++ b/src/bookmarks.h

@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */



#pragma once



+#include "defs.h"

#include <the_Foundation/hash.h>

#include <the_Foundation/ptrarray.h>

#include <the_Foundation/string.h>

@@ -97,6 +98,8 @@ typedef int (*iBookmarksCompareFunc) (const iBookmark **, const iBookmark **)

void clear_Bookmarks (iBookmarks *);

void load_Bookmarks (iBookmarks *, const char *dirPath);

void save_Bookmarks (const iBookmarks *, const char *dirPath);

+void serialize_Bookmarks (const iBookmarks *, iStream *outs);

+void deserialize_Bookmarks (iBookmarks *, iStream *ins, enum iImportMethod);



uint32_t add_Bookmarks (iBookmarks *, const iString *url, const iString *title,

                                      const iString *tags, iChar icon);

diff --git a/src/defs.h b/src/defs.h

index fed7fe82..93db7fa0 100644

--- a/src/defs.h

+++ b/src/defs.h

@@ -31,6 +31,12 @@ enum iSourceFormat {

 markdown_SourceFormat,

};



+enum iImportMethod {

+ none_ImportMethod = 0,

+ ifMissing_ImportMethod,

+ all_ImportMethod,

+};

+

enum iFileVersion {

 initial_FileVersion                 = 0,

 addedResponseTimestamps_FileVersion = 1,

@@ -127,7 +133,8 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {

#define backArrow_Icon "\U0001f870"

#define forwardArrow_Icon "\U0001f872"

#define upArrow_Icon "\U0001f871"

-#define upArrowBar_Icon "\u2912"

+#define upArrowBar_Icon "\u2b71"

+#define keyUpArrow_Icon "\u2191"

#define downArrowBar_Icon "\u2913"

#define rightArrowWhite_Icon "\u21e8"

#define rightArrow_Icon "\u279e"

@@ -152,7 +159,7 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {

#define check_Icon "\u2714"

#define ballotChecked_Icon "\u2611"

#define ballotUnchecked_Icon "\u2610"

-#define inbox_Icon "\U0001f4e5"

+#define import_Icon "\U0001f4e5"

#define book_Icon "\U0001f56e"

#define bookmark_Icon "\U0001f516"

#define folder_Icon "\U0001f4c1"

diff --git a/src/export.c b/src/export.c

new file mode 100644

index 00000000..a343020a

--- /dev/null

+++ b/src/export.c

@@ -0,0 +1,205 @@

+/* Copyright 2022 Jaakko Keränen jaakko.keranen@iki.fi

+

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+

+1. Redistributions of source code must retain the above copyright notice, this

+ list of conditions and the following disclaimer.

+2. Redistributions in binary form must reproduce the above copyright notice,

+ this list of conditions and the following disclaimer in the documentation

+ and/or other materials provided with the distribution.

+

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+

+#include "export.h"

+

+#include "app.h"

+#include "bookmarks.h"

+#include "gmcerts.h"

+#include "sitespec.h"

+#include "visited.h"

+

+#include <the_Foundation/buffer.h>

+#include <the_Foundation/file.h>

+#include <the_Foundation/fileinfo.h>

+#include <the_Foundation/path.h>

+#include <the_Foundation/time.h>

+

+const char *mimeType_Export = "application/lagrange-export+zip";

+

+struct Impl_Export {

+ iArchive *arch;

+};

+

+iDefineTypeConstruction(Export)

+ 

+static const char *metadataEntryName_Export_ = "lagrange-export.ini";

+ 

+void init_Export(iExport *d) {

+ d->arch = new_Archive();

+}

+

+void deinit_Export(iExport *d) {

+ iRelease(d->arch);

+}

+

+void generate_Export(iExport *d) {

+ openWritable_Archive(d->arch);

+ iBuffer *buf = new_Buffer();

+ iString *meta = new_String();

+ iDate today;

+ iTime now;

+ initCurrent_Date(&today);

+ initCurrent_Time(&now);

+ format_String(meta,

+ "# Lagrange user data exported on %s\n"

+ "version = "" LAGRANGE_APP_VERSION ""\n"

+ "timestamp = %llu\n",

+ cstrCollect_String(format_Date(&today, "%Y-%m-%d %H:%M")),

+ (unsigned long long) integralSeconds_Time(&now));

+ /* Bookmarks. */ {

+ openEmpty_Buffer(buf);

+ serialize_Bookmarks(bookmarks_App(), stream_Buffer(buf));

+ setDataCStr_Archive(d->arch, "bookmarks.ini", data_Buffer(buf));

+ close_Buffer(buf);

+ }

+ /* Identities. */ {

+ iBuffer *buf2 = new_Buffer();

+ openEmpty_Buffer(buf2);

+ openEmpty_Buffer(buf);

+ serialize_GmCerts(certs_App(), stream_Buffer(buf), stream_Buffer(buf2));

+ setDataCStr_Archive(d->arch, "trusted.txt", data_Buffer(buf));

+ setDataCStr_Archive(d->arch, "idents.lgr", data_Buffer(buf2));

+ iRelease(buf2);

+ iForEach(DirFileInfo,

+ info,

+ iClob(new_DirFileInfo(collect_String(concatCStr_Path(dataDir_App(), "idents"))))) {

+ const iString *idPath = path_FileInfo(info.value);

+ const iRangecc baseName = baseName_Path(idPath);

+ if (!startsWith_Rangecc(baseName, ".") &&

+ (endsWith_Rangecc(baseName, ".crt") || endsWith_Rangecc(baseName, ".key"))) {

+ iFile *f = new_File(idPath);

+ if (open_File(f, readOnly_FileMode)) {

+ setData_Archive(d->arch,

+ collectNewFormat_String("idents/%s", cstr_Rangecc(baseName)),

+ collect_Block(readAll_File(f)));

+ }

+ iRelease(f);

+ }

+ }

+ close_Buffer(buf);

+ }

+ /* Site-specific settings. */ {

+ openEmpty_Buffer(buf);

+ serialize_SiteSpec(stream_Buffer(buf));

+ setDataCStr_Archive(d->arch, "sitespec.ini", data_Buffer(buf));

+ close_Buffer(buf);

+ }

+ /* History of visited URLs. */ {

+ openEmpty_Buffer(buf);

+ serialize_Visited(visited_App(), stream_Buffer(buf));

+ setDataCStr_Archive(d->arch, "visited.txt", data_Buffer(buf));

+ close_Buffer(buf);

+ } 

+ /* Export metadata. */

+ setDataCStr_Archive(d->arch, metadataEntryName_Export_, utf8_String(meta));

+ delete_String(meta);

+ iRelease(buf);

+}

+

+iBool load_Export(iExport *d, const iArchive *archive) {

+ if (!detect_Export(archive)) {

+ return iFalse;

+ }

+ iRelease(d->arch);

+ d->arch = ref_Object(archive);

+ /* TODO: Check that at least one of the expected files is there. */

+ return iTrue;

+}

+

+iBuffer *openEntryBuffer_Export_(const iExport *d, const char *entryPath) {

+ iBuffer *buf = new_Buffer();

+ if (open_Buffer(buf, dataCStr_Archive(d->arch, entryPath))) {

+ return buf;

+ }

+ iRelease(buf);

+ return NULL;

+}

+

+void import_Export(const iExport *d, enum iImportMethod bookmarks, enum iImportMethod identities,

+ enum iImportMethod trusted, enum iImportMethod visited,

+ enum iImportMethod siteSpec) {

+ if (bookmarks) {

+ iBuffer *buf = openEntryBuffer_Export_(d, "bookmarks.ini");

+ if (buf) {

+ deserialize_Bookmarks(bookmarks_App(), stream_Buffer(buf), bookmarks);

+ iRelease(buf);

+ postCommand_App("bookmarks.changed");

+ }

+ }

+ if (trusted) {

+ iBuffer *buf = openEntryBuffer_Export_(d, "trusted.txt");

+ if (buf) {

+ deserializeTrusted_GmCerts(certs_App(), stream_Buffer(buf), trusted);

+ iRelease(buf);

+ }

+ }

+ if (identities) {

+ /* First extract any missing .crt/.key files to the idents directory. */

+ const iString *identsDir = collect_String(concatCStr_Path(dataDir_App(), "idents"));

+ iConstForEach(StringSet, i,

+ iClob(listDirectory_Archive(d->arch, collectNewCStr_String("idents/")))) {

+ iString *dataPath = concatCStr_Path(identsDir, cstr_Rangecc(baseName_Path(i.value)));

+ if (identities == all_ImportMethod || !fileExists_FileInfo(dataPath)) {

+ iFile *f = new_File(dataPath);

+ if (open_File(f, writeOnly_FileMode)) {

+ write_File(f, data_Archive(d->arch, i.value));

+ }

+ iRelease(f);

+ }

+ delete_String(dataPath);

+ }

+ iBuffer *buf = openEntryBuffer_Export_(d, "idents.lgr");

+ if (buf) {

+ deserializeIdentities_GmCerts(certs_App(), stream_Buffer(buf), identities);

+ iRelease(buf);

+ postCommand_App("idents.changed");

+ }

+ }

+ if (visited) {

+ iBuffer *buf = openEntryBuffer_Export_(d, "visited.txt");

+ if (buf) {

+ deserialize_Visited(visited_App(), stream_Buffer(buf), iTrue /* keep latest */);

+ iRelease(buf);

+ postCommand_App("visited.changed");

+ }

+ }

+ if (siteSpec) {

+ iBuffer *buf = openEntryBuffer_Export_(d, "sitespec.ini");

+ if (buf) {

+ deserialize_SiteSpec(stream_Buffer(buf), siteSpec);

+ iRelease(buf);

+ }

+ }

+}

+

+iBool detect_Export(const iArchive *d) {

+ if (entryCStr_Archive(d, metadataEntryName_Export_)) {

+ return iTrue;

+ }

+ /* TODO: Additional checks? */

+ return iFalse;

+}

+

+const iArchive *archive_Export(const iExport *d) {

+ return d->arch;

+}

diff --git a/src/export.h b/src/export.h

new file mode 100644

index 00000000..149a08c8

--- /dev/null

+++ b/src/export.h

@@ -0,0 +1,44 @@

+/* Copyright 2022 Jaakko Keränen jaakko.keranen@iki.fi

+

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+

+1. Redistributions of source code must retain the above copyright notice, this

+ list of conditions and the following disclaimer.

+2. Redistributions in binary form must reproduce the above copyright notice,

+ this list of conditions and the following disclaimer in the documentation

+ and/or other materials provided with the distribution.

+

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND

+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED

+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR

+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON

+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT

+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS

+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

+

+#pragma once

+

+#include "defs.h"

+#include <the_Foundation/archive.h>

+

+extern const char *mimeType_Export;

+

+iDeclareType(Export)

+iDeclareTypeConstruction(Export) 

+ 

+void generate_Export (iExport *);

+iBool load_Export (iExport *, const iArchive *archive);

+void import_Export (const iExport *,

+ enum iImportMethod bookmarks,

+ enum iImportMethod identities,

+ enum iImportMethod trusted,

+ enum iImportMethod visited,

+ enum iImportMethod siteSpec);

+

+iBool detect_Export (const iArchive *);

+

+const iArchive * archive_Export (const iExport *);

diff --git a/src/gmcerts.c b/src/gmcerts.c

index 7b05103b..dc36cb59 100644

--- a/src/gmcerts.c

+++ b/src/gmcerts.c

@@ -36,7 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include <the_Foundation/time.h>

#include <ctype.h>



-static const char *filename_GmCerts_ = "trusted.2.txt";

+static const char *trustedFilename_GmCerts_ = "trusted.2.txt";

static const char *identsDir_GmCerts_ = "idents";

static const char *oldIdentsFilename_GmCerts_ = "idents.binary";

static const char *identsFilename_GmCerts_ = "idents.lgr";

@@ -248,26 +248,8 @@ static const char *magicIdentity_GmCerts_ = "iden";



iDefineTypeConstructionArgs(GmCerts, (const char *saveDir), saveDir)



-void saveIdentities_GmCerts(const iGmCerts *d) {

- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)));

- if (open_File(f, writeOnly_FileMode)) {

- writeData_File(f, magicIdMeta_GmCerts_, 4);

- writeU32_File(f, idents_FileVersion); /* version */

- iConstForEach(PtrArray, i, &d->idents) {

- const iGmIdentity *ident = i.ptr;

- if (~ident->flags & temporary_GmIdentityFlag) {

- writeData_File(f, magicIdentity_GmCerts_, 4);

- serialize_GmIdentity(ident, stream_File(f));

- }

- }

- }

- iRelease(f);

-}

-

-static void save_GmCerts_(const iGmCerts *d) {

- iBeginCollect();

- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));

- if (open_File(f, writeOnly_FileMode | text_FileMode)) {

+void serialize_GmCerts(const iGmCerts *d, iStream *trusted, iStream *identsMeta) {

+ if (trusted) {

     iString line;

     init_String(&line);

     iConstForEach(StringHash, i, d->trusted) {

@@ -277,57 +259,39 @@ static void save_GmCerts_(const iGmCerts *d) {

                       cstr_String(key_StringHashConstIterator(&i)),

                       integralSeconds_Time(&trust->validUntil),

                       cstrCollect_String(hexEncode_Block(&trust->fingerprint)));

- write_File(f, &line.chars);

+ write_Stream(trusted, &line.chars);

     }

- deinit_String(&line);

+ deinit_String(&line); 

 }

- iRelease(f);

- iEndCollect();

-}

-

-static void loadIdentities_GmCerts_(iGmCerts *d) {

- const iString *oldPath = collect_String(concatCStr_Path(&d->saveDir, oldIdentsFilename_GmCerts_));

- const iString *path = collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_));

- iFile *f = iClob(new_File(fileExists_FileInfo(path) ? path : oldPath));

- if (open_File(f, readOnly_FileMode)) {

- char magic[4];

- readData_File(f, sizeof(magic), magic);

- if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) {

- printf("%s: format not recognized\n", cstr_String(path_File(f)));

- return;

- }

- const uint32_t version = readU32_File(f);

- if (version > latest_FileVersion) {

- printf("%s: unsupported version\n", cstr_String(path_File(f)));

- return;

- }

- setVersion_Stream(stream_File(f), version);

- while (!atEnd_File(f)) {

- readData_File(f, sizeof(magic), magic);

- if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) {

- iGmIdentity *id = new_GmIdentity();

- deserialize_GmIdentity(id, stream_File(f));

- pushBack_PtrArray(&d->idents, id);

- }

- else {

- printf("%s: invalid file contents\n", cstr_String(path_File(f)));

- break;

+ if (identsMeta) {

+ writeData_Stream(identsMeta, magicIdMeta_GmCerts_, 4);

+ writeU32_Stream(identsMeta, idents_FileVersion); /* version */

+ iConstForEach(PtrArray, i, &d->idents) {

+ const iGmIdentity *ident = i.ptr;

+ if (~ident->flags & temporary_GmIdentityFlag) {

+ writeData_Stream(identsMeta, magicIdentity_GmCerts_, 4);

+ serialize_GmIdentity(ident, identsMeta);

         }

- }

+ } 

 }

}



-iGmIdentity *findIdentity_GmCerts(iGmCerts *d, const iBlock *fingerprint) {

- if (isEmpty_Block(fingerprint)) {

- return NULL;

+void saveIdentities_GmCerts(const iGmCerts *d) {

+ iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_)));

+ if (open_File(f, writeOnly_FileMode)) {

+ serialize_GmCerts(d, NULL, stream_File(f));

 }

- iForEach(PtrArray, i, &d->idents) {

- iGmIdentity *ident = i.ptr;

- if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */

- return ident;

- }

+ iRelease(f);

+}

+

+static void save_GmCerts_(const iGmCerts *d) {

+ iBeginCollect();

+ iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, trustedFilename_GmCerts_)));

+ if (open_File(f, writeOnly_FileMode | text_FileMode)) {

+ serialize_GmCerts(d, stream_File(f), NULL); 

 }

- return NULL;

+ iRelease(f);

+ iEndCollect();

}



static void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crtPath) {

@@ -354,53 +318,127 @@ static void loadIdentityFromCertificate_GmCerts_(iGmCerts *d, const iString *crt

 delete_Block(finger);

}



-static void load_GmCerts_(iGmCerts *d) {

- iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, filename_GmCerts_)));

- if (open_File(f, readOnly_FileMode | text_FileMode)) {

- iRegExp * pattern = new_RegExp("([^\s]+) ([0-9]+) ([a-z0-9]+)", 0);

- const iRangecc src = range_Block(collect_Block(readAll_File(f)));

- iRangecc line = iNullRange;

- while (nextSplit_Rangecc(src, "\n", &line)) {

- iRegExpMatch m;

- init_RegExpMatch(&m);

- if (matchRange_RegExp(pattern, line, &m)) {

- const iRangecc key = capturedRange_RegExpMatch(&m, 1);

- const iRangecc until = capturedRange_RegExpMatch(&m, 2);

- const iRangecc fp = capturedRange_RegExpMatch(&m, 3);

- time_t sec;

- sscanf(until.start, "%ld", &sec);

- iDate untilDate;

- initSinceEpoch_Date(&untilDate, sec);

- insert_StringHash(d->trusted,

- collect_String(newRange_String(key)),

- new_TrustEntry(collect_Block(hexDecode_Rangecc(fp)),

- &untilDate));

- }

+static void loadIdentityCertsAndDiscardInvalid_GmCerts_(iGmCerts *d) {

+ const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_));

+ if (!fileExists_FileInfo(idDir)) {

+ makeDirs_Path(idDir);

+ }

+ iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) {

+ const iFileInfo *entry = i.value;

+ if (endsWithCase_String(path_FileInfo(entry), ".crt")) {

+ loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry));

     }

- iRelease(pattern);

 }

- iRelease(f);

- /* Load all identity certificates. */ {

- loadIdentities_GmCerts_(d);

- const iString *idDir = collect_String(concatCStr_Path(&d->saveDir, identsDir_GmCerts_));

- if (!fileExists_FileInfo(idDir)) {

- makeDirs_Path(idDir);

+ /* Remove certificates whose crt/key files were missing. */

+ iForEach(PtrArray, j, &d->idents) {

+ iGmIdentity *ident = j.ptr;

+ if (!isValid_GmIdentity_(ident)) {

+ delete_GmIdentity(ident);

+ remove_PtrArrayIterator(&j);

     }

- iForEach(DirFileInfo, i, iClob(directoryContents_FileInfo(iClob(new_FileInfo(idDir))))) {

- const iFileInfo *entry = i.value;

- if (endsWithCase_String(path_FileInfo(entry), ".crt")) {

- loadIdentityFromCertificate_GmCerts_(d, path_FileInfo(entry));

+ } 

+}

+

+iBool deserializeIdentities_GmCerts(iGmCerts *d, iStream *ins, enum iImportMethod method) {

+ char magic[4];

+ readData_Stream(ins, sizeof(magic), magic);

+ if (memcmp(magic, magicIdMeta_GmCerts_, sizeof(magic))) {

+ fprintf(stderr, "[GmCerts] idents file format not recognized\n");

+ return iFalse;

+ }

+ const uint32_t version = readU32_Stream(ins);

+ if (version > latest_FileVersion) {

+ fprintf(stderr, "[GmCerts] unsupported version (%u)\n", version);

+ return iFalse;

+ }

+ setVersion_Stream(ins, version);

+ while (!atEnd_Stream(ins)) {

+ readData_Stream(ins, sizeof(magic), magic);

+ if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) {

+ iGmIdentity *id = new_GmIdentity();

+ deserialize_GmIdentity(id, ins);

+ if (method == all_ImportMethod ||

+ (method == ifMissing_ImportMethod && !findIdentity_GmCerts(d, &id->fingerprint))) {

+ pushBack_PtrArray(&d->idents, id);

+ }

+ else {

+ delete_GmIdentity(id);

         }

     }

- /* Remove certificates whose crt/key files were missing. */

- iForEach(PtrArray, j, &d->idents) {

- iGmIdentity *ident = j.ptr;

- if (!isValid_GmIdentity_(ident)) {

- delete_GmIdentity(ident);

- remove_PtrArrayIterator(&j);

+ else {

+ fprintf(stderr, "[GmCerts] invalid idents file\n");

+ return iFalse;

+ }

+ }

+ loadIdentityCertsAndDiscardInvalid_GmCerts_(d);

+ return iTrue;

+}

+

+static void loadIdentities_GmCerts_(iGmCerts *d) {

+ const iString *oldPath = collect_String(concatCStr_Path(&d->saveDir, oldIdentsFilename_GmCerts_));

+ const iString *path = collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_));

+ iFile *f = iClob(new_File(fileExists_FileInfo(path) ? path : oldPath));

+ if (open_File(f, readOnly_FileMode)) {

+ deserializeIdentities_GmCerts(d, stream_File(f), all_ImportMethod);

+ }

+ else {

+ /* In any case, load any .crt/.key files that may be present in the "idents" dir. */

+ loadIdentityCertsAndDiscardInvalid_GmCerts_(d); 

+ }

+}

+

+iGmIdentity *findIdentity_GmCerts(iGmCerts *d, const iBlock *fingerprint) {

+ if (isEmpty_Block(fingerprint)) {

+ return NULL;

+ }

+ iForEach(PtrArray, i, &d->idents) {

+ iGmIdentity *ident = i.ptr;

+ if (cmp_Block(fingerprint, &ident->fingerprint) == 0) { /* TODO: could use a hash */

+ return ident;

+ }

+ }

+ return NULL;

+}

+

+void deserializeTrusted_GmCerts(iGmCerts *d, iStream *ins, enum iImportMethod method) {

+ iRegExp * pattern = new_RegExp("([^\s]+) ([0-9]+) ([a-z0-9]+)", 0);

+ const iRangecc src = range_Block(collect_Block(readAll_Stream(ins)));

+ iRangecc line = iNullRange;

+ lock_Mutex(d->mtx);

+ while (nextSplit_Rangecc(src, "\n", &line)) {

+ iRegExpMatch m;

+ init_RegExpMatch(&m);

+ if (matchRange_RegExp(pattern, line, &m)) {

+ iBeginCollect();

+ const iRangecc key = capturedRange_RegExpMatch(&m, 1);

+ const iRangecc until = capturedRange_RegExpMatch(&m, 2);

+ const iRangecc fp = capturedRange_RegExpMatch(&m, 3);

+ time_t sec;

+ sscanf(until.start, "%ld", &sec);

+ iDate untilDate;

+ initSinceEpoch_Date(&untilDate, sec);

+ /* TODO: import method? */

+ const iString *hashKey = collect_String(newRange_String(key));

+ if (method == all_ImportMethod ||

+ (method == ifMissing_ImportMethod && !contains_StringHash(d->trusted, hashKey))) {

+ insert_StringHash(d->trusted,

+ hashKey,

+ new_TrustEntry(collect_Block(hexDecode_Rangecc(fp)), &untilDate));

         }

+ iEndCollect();

     }

 }

+ unlock_Mutex(d->mtx);

+ iRelease(pattern);

+}

+

+static void load_GmCerts_(iGmCerts *d) {

+ iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, trustedFilename_GmCerts_)));

+ if (open_File(f, readOnly_FileMode | text_FileMode)) {

+ deserializeTrusted_GmCerts(d, stream_File(f), all_ImportMethod);

+ }

+ iRelease(f);

+ loadIdentities_GmCerts_(d);

}



iBool verify_GmCerts_(iTlsRequest *request, const iTlsCertificate *cert, int depth) {

diff --git a/src/gmcerts.h b/src/gmcerts.h

index 6ece1954..b451a690 100644

--- a/src/gmcerts.h

+++ b/src/gmcerts.h

@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */



#pragma once



+#include "defs.h"

#include <the_Foundation/ptrarray.h>

#include <the_Foundation/stringset.h>

#include <the_Foundation/tlsrequest.h>

@@ -87,6 +88,9 @@ void importIdentity_GmCerts (iGmCerts *, iTlsCertificate *cert,

                                          const iString *notes); /* takes ownership */

void deleteIdentity_GmCerts (iGmCerts *, iGmIdentity *identity);

void saveIdentities_GmCerts (const iGmCerts *);

+void serialize_GmCerts (const iGmCerts *, iStream *trusted, iStream *identsMeta);

+void deserializeTrusted_GmCerts (iGmCerts *, iStream *ins, enum iImportMethod method);

+iBool deserializeIdentities_GmCerts (iGmCerts *, iStream *ins, enum iImportMethod method);



const iString * certificatePath_GmCerts (const iGmCerts *, const iGmIdentity *identity);



diff --git a/src/gmrequest.c b/src/gmrequest.c

index 55b7b471..aa55aca2 100644

--- a/src/gmrequest.c

+++ b/src/gmrequest.c

@@ -711,7 +711,7 @@ void submit_GmRequest(iGmRequest *d) {

         iString *page = collectNew_String();

         iString *parentDir = collectNewRange_String(dirName_Path(path));

#if !defined (iPlatformMobile)

- appendFormat_String(page, "=> %s " upArrow_Icon " %s" iPathSeparator "\n\n",

+ appendFormat_String(page, "=> %s " keyUpArrow_Icon " %s" iPathSeparator "\n\n",

                             cstrCollect_String(makeFileUrl_String(parentDir)),

                             cstr_String(parentDir));

#endif

@@ -792,14 +792,14 @@ void submit_GmRequest(iGmRequest *d) {

                         if (!equal_Rangecc(parentDir, ".")) {

                             /* A subdirectory. */

                             appendFormat_String(page,

- "=> ../ " upArrow_Icon " %s" iPathSeparator

+ "=> ../ " keyUpArrow_Icon " %s" iPathSeparator

                                                 "\n",

                                                 cstr_Rangecc(parentDir));

                         }

                         else {

                             /* Top-level directory. */

                             appendFormat_String(page,

- "=> %s/ " upArrow_Icon " Root\n",

+ "=> %s/ " keyUpArrow_Icon " Root\n",

                                                 cstr_String(containerUrl));

                         }

                         appendFormat_String(page, "# %s\n\n", cstr_Rangecc(baseName_Path(collectNewRange_String(curDir))));

diff --git a/src/sitespec.c b/src/sitespec.c

index 0db471d8..5f29e55f 100644

--- a/src/sitespec.c

+++ b/src/sitespec.c

@@ -75,6 +75,7 @@ struct Impl_SiteSpec {

 iString     saveDir;

 iStringHash sites;

 iSiteParams *loadParams;

+ enum iImportMethod loadMethod;

};



static iSiteSpec siteSpec_;

@@ -127,7 +128,10 @@ static void handleIniTable_SiteSpec_(void *context, const iString *table, iBool

 }

 else {

     iAssert(d->loadParams != NULL);

- insert_StringHash(&d->sites, table, d->loadParams);

+ if (d->loadMethod == all_ImportMethod ||

+ (d->loadMethod == ifMissing_ImportMethod && !contains_StringHash(&d->sites, table))) {

+ insert_StringHash(&d->sites, table, d->loadParams);

+ }

     iReleasePtr(&d->loadParams);

 }

}

@@ -163,62 +167,74 @@ static void handleIniKeyValue_SiteSpec_(void *context, const iString *table, con

}



static iBool load_SiteSpec_(iSiteSpec *d) {

- iBool ok = iFalse;

+ iBool ok = iFalse; 

 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, fileName_SiteSpec_)));

 if (open_File(f, readOnly_FileMode | text_FileMode)) {

- iTomlParser *toml = new_TomlParser();

- setHandlers_TomlParser(toml, handleIniTable_SiteSpec_, handleIniKeyValue_SiteSpec_, d);

- ok = parse_TomlParser(toml, collect_String(readString_File(f)));

- delete_TomlParser(toml);

+ ok = deserialize_SiteSpec(stream_File(f), all_ImportMethod);

 }

 iRelease(f);

 iAssert(d->loadParams == NULL);

 return ok;

}



+iBool deserialize_SiteSpec(iStream *ins, enum iImportMethod loadMethod) {

+ iSiteSpec *d = &siteSpec_;

+ d->loadMethod = loadMethod;

+ iTomlParser *toml = new_TomlParser();

+ setHandlers_TomlParser(toml, handleIniTable_SiteSpec_, handleIniKeyValue_SiteSpec_, d);

+ iBool ok = parse_TomlParser(toml, collect_String(readString_Stream(ins)));

+ delete_TomlParser(toml);

+ return ok;

+}

+

+void serialize_SiteSpec(iStream *out) {

+ iSiteSpec *d = &siteSpec_;

+ iString *buf = new_String();

+ iConstForEach(StringHash, i, &d->sites) {

+ iBeginCollect();

+ const iBlock * key = &i.value->keyBlock;

+ const iSiteParams *params = i.value->object;

+ clear_String(buf);

+ if (params->titanPort) {

+ appendFormat_String(buf, "titanPort = %u\n", params->titanPort);

+ }

+ if (!isEmpty_String(&params->titanIdentity)) {

+ appendFormat_String(

+ buf, "titanIdentity = "%s"\n", cstr_String(&params->titanIdentity));

+ }

+ if (params->dismissWarnings) {

+ appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings);

+ }

+ if (!isEmpty_StringArray(&params->usedIdentities)) {

+ appendFormat_String(

+ buf,

+ "usedIdentities = "%s"\n",

+ cstrCollect_String(joinCStr_StringArray(&params->usedIdentities, " ")));

+ }

+ if (!isEmpty_String(&params->paletteSeed)) {

+ appendCStr_String(buf, "paletteSeed = "");

+ append_String(buf, collect_String(quote_String(&params->paletteSeed, iFalse)));

+ appendCStr_String(buf, ""\n");

+ }

+ if (!params->tlsSessionCache) {

+ appendCStr_String(buf, "tlsSessionCache = false\n");

+ }

+ if (!isEmpty_String(buf)) {

+ writeData_Stream(out, "[", 1);

+ writeData_Stream(out, constData_Block(key), size_Block(key));

+ writeData_Stream(out, "]\n", 2);

+ appendCStr_String(buf, "\n");

+ write_Stream(out, utf8_String(buf));

+ }

+ iEndCollect();

+ }

+ delete_String(buf); 

+}

+

static void save_SiteSpec_(iSiteSpec *d) {

 iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, fileName_SiteSpec_)));

 if (open_File(f, writeOnly_FileMode | text_FileMode)) {

- iString *buf = new_String();

- iConstForEach(StringHash, i, &d->sites) {

- iBeginCollect();

- const iBlock * key = &i.value->keyBlock;

- const iSiteParams *params = i.value->object;

- clear_String(buf);

- if (params->titanPort) {

- appendFormat_String(buf, "titanPort = %u\n", params->titanPort);

- }

- if (!isEmpty_String(&params->titanIdentity)) {

- appendFormat_String(

- buf, "titanIdentity = "%s"\n", cstr_String(&params->titanIdentity));

- }

- if (params->dismissWarnings) {

- appendFormat_String(buf, "dismissWarnings = 0x%x\n", params->dismissWarnings);

- }

- if (!isEmpty_StringArray(&params->usedIdentities)) {

- appendFormat_String(

- buf,

- "usedIdentities = "%s"\n",

- cstrCollect_String(joinCStr_StringArray(&params->usedIdentities, " ")));

- }

- if (!isEmpty_String(&params->paletteSeed)) {

- appendCStr_String(buf, "paletteSeed = "");

- append_String(buf, collect_String(quote_String(&params->paletteSeed, iFalse)));

- appendCStr_String(buf, ""\n");

- }

- if (!params->tlsSessionCache) {

- appendCStr_String(buf, "tlsSessionCache = false\n");

- }

- if (!isEmpty_String(buf)) {

- writeData_File(f, "[", 1);

- writeData_File(f, constData_Block(key), size_Block(key));

- writeData_File(f, "]\n", 2);

- appendCStr_String(buf, "\n");

- write_File(f, utf8_String(buf));

- }

- iEndCollect();

- }

- delete_String(buf);

+ serialize_SiteSpec(stream_File(f));

 }

 iRelease(f);

}

diff --git a/src/sitespec.h b/src/sitespec.h

index 15219c62..a12bdb00 100644

--- a/src/sitespec.h

+++ b/src/sitespec.h

@@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */



#pragma once



+#include "defs.h"

#include <the_Foundation/stringarray.h>



iDeclareType(SiteSpec)

@@ -38,6 +39,9 @@ enum iSiteSpecKey {

void init_SiteSpec (const char *saveDir);

void deinit_SiteSpec (void);



+void serialize_SiteSpec (iStream *);

+iBool deserialize_SiteSpec (iStream *, enum iImportMethod);

+

/* changes saved immediately */

void setValue_SiteSpec (const iString *site, enum iSiteSpecKey key, int value); 

void setValueString_SiteSpec (const iString *site, enum iSiteSpecKey key, const iString *value);

diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c

index d6c63a30..14fd2433 100644

--- a/src/ui/documentwidget.c

+++ b/src/ui/documentwidget.c

@@ -31,6 +31,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "bookmarks.h"

#include "command.h"

#include "defs.h"

+#include "export.h"

#include "gempub.h"

#include "gmcerts.h"

#include "gmdocument.h"

@@ -2415,6 +2416,9 @@ static const char *zipPageHeading_(const iRangecc mime) {

 else if (equalCase_Rangecc(mime, mimeType_FontPack)) {

     return fontpack_Icon " Fontpack";

 }

+ else if (equalCase_Rangecc(mime, mimeType_Export)) {

+ return package_Icon " ${heading.archive.userdata}";

+ }

 iRangecc type = iNullRange;

 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */

 nextSplit_Rangecc(mime, "/", &type);

@@ -2701,13 +2705,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,

                      (equal_Rangecc(param, "application/zip") ||

                      (startsWith_Rangecc(param, "application/") &&

                       endsWithCase_Rangecc(param, "+zip")))) {

+ iArray *footerItems = collectNew_Array(sizeof(iMenuItem));

                 clear_String(&str);

                 docFormat = gemini_SourceFormat;

                 setRange_String(&d->sourceMime, param);

+ iArchive *zip = new_Archive();

+ openData_Archive(zip, &response->body);

                 if (equal_Rangecc(param, mimeType_FontPack)) {

                     /* Show some information about fontpacks, and set up footer actions. */

- iArchive *zip = iClob(new_Archive());

- if (openData_Archive(zip, &response->body)) {

+ if (isOpen_Archive(zip)) {

                         iFontPack *fp = new_FontPack();

                         setUrl_FontPack(fp, d->mod.url);

                         setStandalone_FontPack(fp, iTrue);

@@ -2726,28 +2732,54 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,

                     }

                 }

                 else {

- format_String(&str, "# %s\n", zipPageHeading_(param));

+ if (detect_Export(zip)) {

+ setCStr_String(&d->sourceMime, mimeType_Export);

+ pushBack_Array(footerItems,

+ &(iMenuItem){ openExt_Icon " ${menu.open.external}",

+ SDLK_RETURN,

+ KMOD_PRIMARY,

+ "document.save extview:1" });

+ }

+ format_String(&str, "# %s\n", zipPageHeading_(range_String(&d->sourceMime)));

                     appendFormat_String(&str,

                                         cstr_Lang("doc.archive"),

                                         cstr_Rangecc(baseName_Path(d->mod.url)));

                     appendCStr_String(&str, "\n");

                 }

+ iRelease(zip);

                 appendCStr_String(&str, "\n");

                 iString *localPath = localFilePathFromUrl_String(d->mod.url);

- if (!localPath) {

+ if (!localPath || !fileExists_FileInfo(localPath)) {

                     iString *key = collectNew_String();

                     toString_Sym(SDLK_s, KMOD_PRIMARY, key);

                     appendFormat_String(&str, "%s\n\n",

                                         format_CStr(cstr_Lang("error.unsupported.suggestsave"),

                                                     cstr_String(key),

                                                     saveToDownloads_Label));

+ pushBack_Array(footerItems,

+ &(iMenuItem){ translateCStr_Lang(download_Icon

+ " " saveToDownloads_Label),

+ 0,

+ 0,

+ "document.save" });

                 }

- delete_String(localPath);

- if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {

- appendFormat_String(&str, "=> %s/ " folder_Icon " ${doc.archive.view}\n",

+ if (localPath && fileExists_FileInfo(localPath)) {

+ if (!cmp_String(&d->sourceMime, mimeType_Export)) {

+ pushFront_Array(footerItems,

+ &(iMenuItem){ import_Icon " ${menu.import}",

+ SDLK_RETURN,

+ 0,

+ format_CStr("!import path:%s",

+ cstr_String(localPath)) });

+ }

+ appendFormat_String(&str,

+ "=> %s/ " folder_Icon " ${doc.archive.view}\n",

                                         cstr_String(withSpacesEncoded_String(d->mod.url)));

                 }

+ delete_String(localPath);

                 translate_Lang(&str);

+ makeFooterButtons_DocumentWidget_(

+ d, constData_Array(footerItems), size_Array(footerItems));

             }

             else if (startsWith_Rangecc(param, "image/") ||

                      startsWith_Rangecc(param, "audio/")) {

@@ -4287,7 +4319,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)

     }

     else if (!isEmpty_Block(&d->sourceContent)) {

         if (argLabel_Command(cmd, "extview")) {

- if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file")) {

+ if (equalCase_Rangecc(urlScheme_String(d->mod.url), "file") &&

+ fileExists_FileInfo(collect_String(localFilePathFromUrl_String(d->mod.url)))) {

                 /* Already a file so just open it directly. */

                 postCommandf_Root(w->root, "!open default:1 url:%s", cstr_String(d->mod.url));

             }

diff --git a/src/ui/util.c b/src/ui/util.c

index c3e39821..608ee116 100644

--- a/src/ui/util.c

+++ b/src/ui/util.c

@@ -29,6 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */

#include "command.h"

#include "defs.h"

#include "documentwidget.h"

+#include "export.h"

#include "feeds.h"

#include "gmutil.h"

#include "inputwidget.h"

@@ -2658,7 +2659,7 @@ iWidget *makePreferences_Widget(void) {

     const iMenuItem identityPanelItems[] = {

         { "title id:sidebar.identities" },

         { "certlist" },

- { "navi.action id:prefs.ident.import text:" inbox_Icon, 0, 0, "ident.import" },

+ { "navi.action id:prefs.ident.import text:" import_Icon, 0, 0, "ident.import" },

         { "navi.action id:prefs.ident.new text:" add_Icon, 0, 0, "ident.new" },

         { NULL }  

     };

@@ -3718,6 +3719,118 @@ iWidget *makeGlyphFinder_Widget(void) {

 return dlg;

}



+static enum iImportMethod checkImportMethod_(const iWidget *dlg, const char *id) {

+ return isSelected_Widget(findChild_Widget(dlg, format_CStr("%s.0", id))) ? none_ImportMethod

+ : isSelected_Widget(findChild_Widget(dlg, format_CStr("%s.1", id)))

+ ? ifMissing_ImportMethod

+ : all_ImportMethod;

+}

+

+static iBool handleUserDataImporterCommands_(iWidget *dlg, const char *cmd) {

+ if (equalWidget_Command(cmd, dlg, "importer.cancel") ||

+ equalWidget_Command(cmd, dlg, "importer.accept")) {

+ if (equal_Command(cmd, "importer.accept")) {

+ /* Compose the final import command. */

+ enum iImportMethod bookmarkMethod = checkImportMethod_(dlg, "importer.bookmark");

+ enum iImportMethod identMethod = checkImportMethod_(dlg, "importer.idents");

+ enum iImportMethod trustedMethod = checkImportMethod_(dlg, "importer.trusted");

+ enum iImportMethod sitespecMethod = checkImportMethod_(dlg, "importer.sitespec");

+ enum iImportMethod visitedMethod =

+ isSelected_Widget(findChild_Widget(dlg, "importer.history")) ? all_ImportMethod

+ : none_ImportMethod;

+ postCommandf_App(

+ "import arg:1 bookmarks:%d idents:%d trusted:%d visited:%d sitespec:%d path:%s",

+ bookmarkMethod,

+ identMethod,

+ trustedMethod,

+ visitedMethod,

+ sitespecMethod,

+ suffixPtr_Command(cmd, "path"));

+ }

+ setupSheetTransition_Mobile(dlg, dialogTransitionDir_Widget(dlg));

+ destroy_Widget(dlg);

+ return iTrue;

+ }

+ else if (equalWidget_Command(cmd, dlg, "importer.selectall")) {

+ postCommand_Widget(findChild_Widget(dlg, "importer.bookmark.1"), "trigger");

+ postCommand_Widget(findChild_Widget(dlg, "importer.trusted.1"), "trigger");

+ postCommand_Widget(findChild_Widget(dlg, "importer.idents.1"), "trigger");

+ postCommand_Widget(findChild_Widget(dlg, "importer.sitespec.1"), "trigger");

+ setToggle_Widget(findChild_Widget(dlg, "importer.history"), iTrue);

+ return iTrue;

+ }

+ return iFalse;

+}

+

+iWidget *makeUserDataImporter_Dialog(const iString *archivePath) {

+ iWidget *dlg;

+ const iMenuItem actions[] = {

+ { "${menu.selectall}", 0, 0, "importer.selectall" },

+ { "---" },

+ { "${cancel}", SDLK_ESCAPE, 0, "importer.cancel" },

+ { uiTextAction_ColorEscape "${import.userdata}",

+ SDLK_RETURN, KMOD_PRIMARY,

+ format_CStr("importer.accept path:%s", cstr_String(archivePath)) },

+ };

+ if (isUsingPanelLayout_Mobile()) {

+ dlg = makePanels_Mobile("importer", (iMenuItem[]){

+ { NULL }

+ }, actions, iElemCount(actions));

+ }

+ else {

+ dlg = makeSheet_Widget("importer");

+ addDialogTitle_(dlg, "${heading.import.userdata}", NULL);

+ iWidget *headings, *values;

+ addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));

+ /* Bookmarks. */

+ addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.bookmarks}")));

+ iWidget *radio = new_Widget(); {

+ addRadioButton_(radio, "importer.bookmark.0", "${dlg.userdata.no}", ".");

+ addRadioButton_(radio, "importer.bookmark.1", "${dlg.userdata.missing}", ".");

+ addRadioButton_(radio, "importer.bookmark.2", "${dlg.userdata.alldup}", ".");

+ }

+ addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

+ /* Site-specific. */

+ addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.sitespec}")));

+ radio = new_Widget(); {

+ addRadioButton_(radio, "importer.sitespec.0", "${dlg.userdata.no}", ".");

+ addRadioButton_(radio, "importer.sitespec.1", "${dlg.userdata.missing}", ".");

+ addRadioButton_(radio, "importer.sitespec.2", "${dlg.userdata.all}", ".");

+ }

+ addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

+ /* Trusted certs. */

+ addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.trusted}")));

+ radio = new_Widget(); {

+ addRadioButton_(radio, "importer.trusted.0", "${dlg.userdata.no}", ".");

+ addRadioButton_(radio, "importer.trusted.1", "${dlg.userdata.missing}", ".");

+ addRadioButton_(radio, "importer.trusted.2", "${dlg.userdata.all}", ".");

+ }

+ addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

+ /* Identities. */

+ addChild_Widget(headings, iClob(makeHeading_Widget("${import.userdata.idents}")));

+ radio = new_Widget(); {

+ addRadioButton_(radio, "importer.idents.0", "${dlg.userdata.no}", ".");

+ addRadioButton_(radio, "importer.idents.1", "${dlg.userdata.missing}", ".");

+ addRadioButton_(radio, "importer.idents.2", "${dlg.userdata.all}", ".");

+ }

+ addChildFlags_Widget(values, iClob(radio), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);

+ addDialogToggle_(headings, values, "${import.userdata.history}", "importer.history");

+ addDialogPadding_(headings, values);

+ addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));

+ addChild_Widget(dlg->root->widget, iClob(dlg));

+ arrange_Widget(dlg);

+ arrange_Widget(dlg);

+ setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | dialogTransitionDir_Widget(dlg));

+ }

+ /* Initialize. */

+ setToggle_Widget(findChild_Widget(dlg, "importer.bookmark.0"), iTrue);

+ setToggle_Widget(findChild_Widget(dlg, "importer.idents.0"), iTrue);

+ setToggle_Widget(findChild_Widget(dlg, "importer.sitespec.0"), iTrue);

+ setToggle_Widget(findChild_Widget(dlg, "importer.trusted.0"), iTrue);

+ setCommandHandler_Widget(dlg, handleUserDataImporterCommands_);

+ return dlg;

+}

+

/----------------------------------------------------------------------------------------------/



void init_PerfTimer(iPerfTimer *d) {

diff --git a/src/ui/util.h b/src/ui/util.h

index 50440137..cda1af61 100644

--- a/src/ui/util.h

+++ b/src/ui/util.h

@@ -346,6 +346,7 @@ iWidget * makeFeedSettings_Widget (uint32_t bookmarkId);

iWidget * makeSiteSpecificSettings_Widget (const iString *url);

iWidget * makeTranslation_Widget (iWidget *parent);

iWidget * makeGlyphFinder_Widget (void);

+iWidget * makeUserDataImporter_Dialog (const iString *archivePath);



const char * languageId_String (const iString *menuItemLabel);

int languageIndex_CStr (const char *langId);

diff --git a/src/ui/window.c b/src/ui/window.c

index f58d7b46..c981e7cf 100644

--- a/src/ui/window.c

+++ b/src/ui/window.c

@@ -87,6 +87,7 @@ static const iMenuItem fileMenuItems_[] = {

 { saveToDownloads_Label, SDLK_s, KMOD_PRIMARY, "document.save" },

 { "---", 0, 0, NULL },

 { "${menu.downloads}", 0, 0, "downloads.open" },

+ { "${menu.export}", 0, 0, "export" },

};



static const iMenuItem editMenuItems_[] = {

diff --git a/src/visited.c b/src/visited.c

index f6299ddb..c1c091e2 100644

--- a/src/visited.c

+++ b/src/visited.c

@@ -72,53 +72,71 @@ void deinit_Visited(iVisited *d) {

 delete_Mutex(d->mtx);

}



-void save_Visited(const iVisited *d, const char *dirPath) {

+void serialize_Visited(const iVisited *d, iStream *out) {

 iString *line = new_String();

+ lock_Mutex(d->mtx);

+ iConstForEach(Array, i, &d->visited.values) {

+ const iVisitedUrl *item = i.value;

+ format_String(line,

+ "%llu %04x %s\n",

+ (unsigned long long) integralSeconds_Time(&item->when),

+ item->flags,

+ cstr_String(&item->url));

+ writeData_Stream(out, cstr_String(line), size_String(line));

+ }

+ unlock_Mutex(d->mtx);

+ delete_String(line);

+}

+

+void save_Visited(const iVisited *d, const char *dirPath) {

 iFile *f = newCStr_File(concatPath_CStr(dirPath, "visited.2.txt"));

 if (open_File(f, writeOnly_FileMode | text_FileMode)) {

- lock_Mutex(d->mtx);

- iConstForEach(Array, i, &d->visited.values) {

- const iVisitedUrl *item = i.value;

- format_String(line,

- "%llu %04x %s\n",

- (unsigned long long) integralSeconds_Time(&item->when),

- item->flags,

- cstr_String(&item->url));

- writeData_File(f, cstr_String(line), size_String(line));

- }

- unlock_Mutex(d->mtx);

+ serialize_Visited(d, stream_File(f));

 }

 iRelease(f);

- delete_String(line);

+}

+

+void deserialize_Visited(iVisited *d, iStream *ins, iBool mergeKeepingLatest) {

+ const iRangecc src = range_Block(collect_Block(readAll_Stream(ins)));

+ iRangecc line = iNullRange;

+ iTime now;

+ initCurrent_Time(&now);

+ lock_Mutex(d->mtx);

+ while (nextSplit_Rangecc(src, "\n", &line)) {

+ if (size_Range(&line) < 8) continue;

+ char *endp = NULL;

+ const unsigned long long ts = strtoull(line.start, &endp, 10);

+ if (ts == 0) break;

+ const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16);

+ const char *urlStart = skipSpace_CStr(endp);

+ iVisitedUrl item;

+ item.when.ts = (struct timespec){ .tv_sec = ts };

+ if (~flags & kept_VisitedUrlFlag &&

+ secondsSince_Time(&now, &item.when) > maxAge_Visited) {

+ continue; /* Too old. */

+ }

+ item.flags = flags;

+ initRange_String(&item.url, (iRangecc){ urlStart, line.end });

+ set_String(&item.url, &item.url);

+ if (mergeKeepingLatest) {

+ /* Check if we already have this. */

+ size_t existingPos;

+ if (locate_SortedArray(&d->visited, &item, &existingPos)) {

+ iVisitedUrl *existing = at_SortedArray(&d->visited, existingPos);

+ max_Time(&existing->when, &item.when);

+ existing->flags = item.flags;

+ continue;

+ }

+ }

+ insert_SortedArray(&d->visited, &item);

+ }

+ unlock_Mutex(d->mtx);

}



void load_Visited(iVisited *d, const char *dirPath) {

 iFile *f = newCStr_File(concatPath_CStr(dirPath, "visited.2.txt"));

 if (open_File(f, readOnly_FileMode | text_FileMode)) {

- lock_Mutex(d->mtx);

- const iRangecc src = range_Block(collect_Block(readAll_File(f)));

- iRangecc line = iNullRange;

- iTime now;

- initCurrent_Time(&now);

- while (nextSplit_Rangecc(src, "\n", &line)) {

- if (size_Range(&line) < 8) continue;

- char *endp = NULL;

- const unsigned long long ts = strtoull(line.start, &endp, 10);

- if (ts == 0) break;

- const uint32_t flags = (uint32_t) strtoul(skipSpace_CStr(endp), &endp, 16);

- const char *urlStart = skipSpace_CStr(endp);

- iVisitedUrl item;

- item.when.ts = (struct timespec){ .tv_sec = ts };

- if (~flags & kept_VisitedUrlFlag &&

- secondsSince_Time(&now, &item.when) > maxAge_Visited) {

- continue; /* Too old. */

- }

- item.flags = flags;

- initRange_String(&item.url, (iRangecc){ urlStart, line.end });

- set_String(&item.url, &item.url);

- insert_SortedArray(&d->visited, &item);

- }

- unlock_Mutex(d->mtx);

+ deserialize_Visited(d, stream_File(f), iFalse /* no merge */);

 }

 iRelease(f);

}

diff --git a/src/visited.h b/src/visited.h

index 0fde1d1f..1484492e 100644

--- a/src/visited.h

+++ b/src/visited.h

@@ -50,6 +50,8 @@ iDeclareTypeConstruction(Visited)

void clear_Visited (iVisited *);

void load_Visited (iVisited *, const char *dirPath);

void save_Visited (const iVisited *, const char *dirPath);

+void serialize_Visited (const iVisited *, iStream *out);

+void deserialize_Visited (iVisited *, iStream *ins, iBool mergeKeepingLatest);



iTime urlVisitTime_Visited (const iVisited *, const iString *url);

void visitUrl_Visited (iVisited *, const iString url, uint16_t visitFlags); / adds URL to the visited URLs set */

Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.12/pcdiff/75197707e0bb149cb9c2e4a983d92fdfb381c17f
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
129.150309 milliseconds
Gemini-to-HTML Time
28.006521 milliseconds

This content has been proxied by September (3851b).