From c61825e8b3c6744576edd4d03ce2a2972d7ec616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=97=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=27=D1=94=D0=B2?= Date: Mon, 14 Apr 2025 03:17:08 +0300 Subject: [PATCH 1/4] Remove savegame object compression Preparation for further changes. Savegames that are not compressed are much bigger, but this is a rather complex algorithm that shouldn't exist. --- src/js/core/read_write_proxy.js | 11 +- src/js/savegame/savegame_compressor.js | 168 ------------------------ src/js/webworkers/compression.worker.js | 4 +- 3 files changed, 4 insertions(+), 179 deletions(-) delete mode 100644 src/js/savegame/savegame_compressor.js diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 2bc31396..949eb802 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -3,7 +3,6 @@ import { Application } from "../application"; /* typehints:end */ import { FsError } from "@/platform/fs_error"; -import { compressObject, decompressObject } from "../savegame/savegame_compressor"; import { asyncCompressor, compressionPrefix } from "./async_compression"; import { IS_DEBUG, globalConfig } from "./config"; import { ExplainedResult } from "./explained_result"; @@ -85,7 +84,7 @@ export class ReadWriteProxy { * @param {object} obj */ static serializeObject(obj) { - const jsonString = JSON.stringify(compressObject(obj)); + const jsonString = JSON.stringify(obj); const checksum = computeCrc(jsonString + salt); return compressionPrefix + compressX64(checksum + jsonString); } @@ -117,8 +116,7 @@ export class ReadWriteProxy { } const parsed = JSON.parse(jsonString); - const decoded = decompressObject(parsed); - return decoded; + return parsed; } /** @@ -180,7 +178,7 @@ export class ReadWriteProxy { .then(rawData => { if (rawData == null) { // So, the file has not been found, use default data - return JSON.stringify(compressObject(this.getDefaultData())); + return JSON.stringify(this.getDefaultData()); } if (rawData.startsWith(compressionPrefix)) { @@ -233,9 +231,6 @@ export class ReadWriteProxy { } }) - // Decompress - .then(compressed => decompressObject(compressed)) - // Verify basic structure .then(contents => { const result = this.internalVerifyBasicStructure(contents); diff --git a/src/js/savegame/savegame_compressor.js b/src/js/savegame/savegame_compressor.js deleted file mode 100644 index d8797bad..00000000 --- a/src/js/savegame/savegame_compressor.js +++ /dev/null @@ -1,168 +0,0 @@ -const charmap = - "!#%&'()*+,-./:;<=>?@[]^_`{|}~¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -let compressionCache = {}; -let decompressionCache = {}; - -/** - * Compresses an integer into a tight string representation - * @param {number} i - * @returns {string} - */ -function compressInt(i) { - // Zero value breaks - i += 1; - - // save `i` as the cache key - // to avoid it being modified by the - // rest of the function. - const cache_key = i; - - if (compressionCache[cache_key]) { - return compressionCache[cache_key]; - } - let result = ""; - do { - result += charmap[i % charmap.length]; - i = Math.floor(i / charmap.length); - } while (i > 0); - return (compressionCache[cache_key] = result); -} - -/** - * Decompresses an integer from its tight string representation - * @param {string} s - * @returns {number} - */ -function decompressInt(s) { - if (decompressionCache[s]) { - return decompressionCache[s]; - } - s = "" + s; - let result = 0; - for (let i = s.length - 1; i >= 0; --i) { - result = result * charmap.length + charmap.indexOf(s.charAt(i)); - } - // Fixes zero value break fix from above - result -= 1; - return (decompressionCache[s] = result); -} - -// Sanity -if (G_IS_DEV) { - for (let i = 0; i < 10000; ++i) { - if (decompressInt(compressInt(i)) !== i) { - throw new Error( - "Bad compression for: " + - i + - " compressed: " + - compressInt(i) + - " decompressed: " + - decompressInt(compressInt(i)) - ); - } - } -} - -/** - * @param {any} obj - * @param {Map} keys - * @param {Map} values - * @returns {any[]|object|number|string} - */ -function compressObjectInternal(obj, keys, values) { - if (Array.isArray(obj)) { - let result = []; - for (let i = 0; i < obj.length; ++i) { - result.push(compressObjectInternal(obj[i], keys, values)); - } - return result; - } else if (typeof obj === "object" && obj !== null) { - let result = {}; - for (const key in obj) { - let index = keys.get(key); - if (index === undefined) { - index = keys.size; - keys.set(key, index); - } - const value = obj[key]; - result[compressInt(index)] = compressObjectInternal(value, keys, values); - } - return result; - } else if (typeof obj === "string") { - let index = values.get(obj); - if (index === undefined) { - index = values.size; - values.set(obj, index); - } - return compressInt(index); - } - return obj; -} - -/** - * @param {Map} hashMap - * @returns {Array} - */ -function indexMapToArray(hashMap) { - const result = new Array(hashMap.size); - hashMap.forEach((index, key) => { - result[index] = key; - }); - return result; -} - -/** - * @param {object} obj - */ -export function compressObject(obj) { - const keys = new Map(); - const values = new Map(); - const data = compressObjectInternal(obj, keys, values); - return { - keys: indexMapToArray(keys), - values: indexMapToArray(values), - data, - }; -} - -/** - * @param {object} obj - * @param {string[]} keys - * @param {any[]} values - * @returns {object} - */ -function decompressObjectInternal(obj, keys = [], values = []) { - if (Array.isArray(obj)) { - let result = []; - for (let i = 0; i < obj.length; ++i) { - result.push(decompressObjectInternal(obj[i], keys, values)); - } - return result; - } else if (typeof obj === "object" && obj !== null) { - let result = {}; - for (const key in obj) { - const realIndex = decompressInt(key); - const value = obj[key]; - result[keys[realIndex]] = decompressObjectInternal(value, keys, values); - } - return result; - } else if (typeof obj === "string") { - const realIndex = decompressInt(obj); - return values[realIndex]; - } - return obj; -} - -/** - * @param {object} obj - */ -export function decompressObject(obj) { - if (obj.keys && obj.values && obj.data) { - const keys = obj.keys; - const values = obj.values; - const result = decompressObjectInternal(obj.data, keys, values); - return result; - } - return obj; -} diff --git a/src/js/webworkers/compression.worker.js b/src/js/webworkers/compression.worker.js index e0414d7c..83862d10 100644 --- a/src/js/webworkers/compression.worker.js +++ b/src/js/webworkers/compression.worker.js @@ -1,7 +1,6 @@ import { globalConfig } from "../core/config"; import { compressX64 } from "../core/lzstring"; import { computeCrc } from "../core/sensitive_utils.encrypt"; -import { compressObject } from "../savegame/savegame_compressor"; self.addEventListener("message", event => { // @ts-ignore @@ -19,8 +18,7 @@ function performJob(job, data) { } case "compressObject": { - const optimized = compressObject(data.obj); - const stringified = JSON.stringify(optimized); + const stringified = JSON.stringify(data.obj); const checksum = computeCrc(stringified + globalConfig.info.file); return data.compressionPrefix + compressX64(checksum + stringified); From 6b7cfa1b1b3dbbdb76c47d782dd6453c761ca06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=97=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=27=D1=94=D0=B2?= Date: Mon, 14 Apr 2025 17:48:35 +0300 Subject: [PATCH 2/4] Pass storage object to ReadWriteProxy Make it slightly easier to replace the storage interface used for app_settings.bin and savegames.bin in case it's ever needed. Savegames always use app.storage for now, but this should be easier to change as well. --- src/js/application.js | 16 ++++++++-------- src/js/core/read_write_proxy.js | 14 +++++++------- src/js/profile/application_settings.js | 15 +++++++++------ src/js/savegame/savegame.js | 6 +++++- src/js/savegame/savegame_manager.js | 11 +++++++++-- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/js/application.js b/src/js/application.js index 71115f54..b61eb923 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -54,21 +54,21 @@ export class Application { this.unloaded = false; + // Platform stuff + this.storage = new Storage(this); + this.platformWrapper = new PlatformWrapperImplElectron(this); + // Global stuff - this.settings = new ApplicationSettings(this); + this.settings = new ApplicationSettings(this, this.storage); this.ticker = new AnimationFrame(); this.stateMgr = new StateManager(this); - this.savegameMgr = new SavegameManager(this); + // NOTE: SavegameManager uses the passed storage, but savegames always + // use Application#storage + this.savegameMgr = new SavegameManager(this, this.storage); this.inputMgr = new InputDistributor(this); this.backgroundResourceLoader = new BackgroundResourcesLoader(this); this.clientApi = new ClientAPI(this); - // Platform dependent stuff - - this.storage = new Storage(this); - - this.platformWrapper = new PlatformWrapperImplElectron(this); - this.sound = new Sound(this); // Track if the window is focused (only relevant for browser) diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 949eb802..a7f03d52 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -1,5 +1,5 @@ /* typehints:start */ -import { Application } from "../application"; +import { Storage } from "@/platform/storage"; /* typehints:end */ import { FsError } from "@/platform/fs_error"; @@ -18,9 +18,9 @@ const salt = globalConfig.info.file; // Helper which only writes / reads if verify() works. Also performs migration export class ReadWriteProxy { - constructor(app, filename) { - /** @type {Application} */ - this.app = app; + constructor(storage, filename) { + /** @type {Storage} */ + this.storage = storage; this.filename = filename; @@ -143,7 +143,7 @@ export class ReadWriteProxy { return asyncCompressor .compressObjectAsync(this.currentData) .then(compressed => { - return this.app.storage.writeFileAsync(this.filename, compressed); + return this.storage.writeFileAsync(this.filename, compressed); }) .then(() => { logger.log("📄 Wrote", this.filename); @@ -158,7 +158,7 @@ export class ReadWriteProxy { readAsync() { // Start read request return ( - this.app.storage + this.storage .readFileAsync(this.filename) // Check for errors during read @@ -297,7 +297,7 @@ export class ReadWriteProxy { * @returns {Promise} */ deleteAsync() { - return this.app.storage.deleteFileAsync(this.filename); + return this.storage.deleteFileAsync(this.filename); } // Internal diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index b454c163..22133d00 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -2,13 +2,13 @@ import { Application } from "../application"; /* typehints:end */ -import { ReadWriteProxy } from "../core/read_write_proxy"; -import { BoolSetting, EnumSetting, RangeSetting, BaseSetting } from "./setting_types"; -import { createLogger } from "../core/logging"; import { ExplainedResult } from "../core/explained_result"; +import { createLogger } from "../core/logging"; +import { ReadWriteProxy } from "../core/read_write_proxy"; import { THEMES, applyGameTheme } from "../game/theme"; -import { T } from "../translations"; import { LANGUAGES } from "../languages"; +import { T } from "../translations"; +import { BaseSetting, BoolSetting, EnumSetting, RangeSetting } from "./setting_types"; const logger = createLogger("application_settings"); @@ -330,8 +330,11 @@ class SettingsStorage { } export class ApplicationSettings extends ReadWriteProxy { - constructor(app) { - super(app, "app_settings.bin"); + constructor(app, storage) { + super(storage, "app_settings.bin"); + + /** @type {Application} */ + this.app = app; this.settingHandles = initializeSettings(); } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index d93c5997..b4054553 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -36,7 +36,11 @@ export class Savegame extends ReadWriteProxy { * @param {SavegameMetadata} param0.metaDataRef Handle to the meta data */ constructor(app, { internalId, metaDataRef }) { - super(app, "savegame-" + internalId + ".bin"); + super(app.storage, "savegame-" + internalId + ".bin"); + + /** @type {Application} */ + this.app = app; + this.internalId = internalId; this.metaDataRef = metaDataRef; diff --git a/src/js/savegame/savegame_manager.js b/src/js/savegame/savegame_manager.js index b70bd851..6ad3b98a 100644 --- a/src/js/savegame/savegame_manager.js +++ b/src/js/savegame/savegame_manager.js @@ -1,3 +1,7 @@ +/* typehints:start */ +import { Application } from "@/application"; +/* typehints:end */ + import { globalConfig } from "../core/config"; import { ExplainedResult } from "../core/explained_result"; import { createLogger } from "../core/logging"; @@ -17,8 +21,11 @@ export const enumLocalSavegameStatus = { }; export class SavegameManager extends ReadWriteProxy { - constructor(app) { - super(app, "savegames.bin"); + constructor(app, storage) { + super(storage, "savegames.bin"); + + /** @type {Application} */ + this.app = app; this.currentData = this.getDefaultData(); } From fc33cc2fbf4ba82636de5c81c7b6079ec9989fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=97=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=27=D1=94=D0=B2?= Date: Tue, 15 Apr 2025 01:57:00 +0300 Subject: [PATCH 3/4] Add dedicated saves storage, new fs job types Keep track of the storage ID in each renderer Storage instance and pass it to the IPC bridge. Jobs are dispatched to the relevant handler (only saves/ for now) and all (de)compression is handled there. Add dedicated fs-job types to read or write and (de)compress data from/to the file picked by the user. Remove redundant utility functions that used web APIs instead. --- electron/package-lock.json | 10 +++ electron/package.json | 1 + electron/src/fsjob.ts | 115 ++++++++++++++++++------ electron/src/index.ts | 9 +- electron/src/ipc.ts | 14 +-- electron/src/storage/interface.ts | 5 ++ electron/src/storage/raw.ts | 16 ++++ electron/src/storage/saves.ts | 54 +++++++++++ src/js/application.js | 6 +- src/js/core/async_compression.js | 101 --------------------- src/js/core/read_write_proxy.js | 89 ++---------------- src/js/core/utils.js | 32 ------- src/js/platform/storage.ts | 53 +++++++---- src/js/savegame/savegame.js | 64 +------------ src/js/states/main_menu.js | 80 ++++++----------- src/js/webworkers/compression.worker.js | 29 ------ 16 files changed, 257 insertions(+), 421 deletions(-) create mode 100644 electron/src/storage/interface.ts create mode 100644 electron/src/storage/raw.ts create mode 100644 electron/src/storage/saves.ts delete mode 100644 src/js/core/async_compression.js delete mode 100644 src/js/webworkers/compression.worker.js diff --git a/electron/package-lock.json b/electron/package-lock.json index 9faab32c..820d1431 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@msgpack/msgpack": "^3.1.1", "semver": "^7.7.1", "zod": "^3.24.2" }, @@ -50,6 +51,15 @@ "semver": "bin/semver.js" } }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.1.tgz", + "integrity": "sha512-DnBpqkMOUGayNVKyTLlkM6ILmU/m/+VUxGkuQlPQVAcvreLz5jn1OlQnWd8uHKL/ZSiljpM12rjRhr51VtvJUQ==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", diff --git a/electron/package.json b/electron/package.json index 13cfffa3..86debdb5 100644 --- a/electron/package.json +++ b/electron/package.json @@ -9,6 +9,7 @@ "start": "tsc && electron ." }, "dependencies": { + "@msgpack/msgpack": "^3.1.1", "semver": "^7.7.1", "zod": "^3.24.2" }, diff --git a/electron/src/fsjob.ts b/electron/src/fsjob.ts index 0e85ea69..9cc3c866 100644 --- a/electron/src/fsjob.ts +++ b/electron/src/fsjob.ts @@ -1,68 +1,129 @@ +import { BrowserWindow, dialog, FileFilter } from "electron"; import fs from "fs/promises"; import path from "path"; import { userData } from "./config.js"; +import { StorageInterface } from "./storage/interface.js"; interface GenericFsJob { - filename: string; + id: string; } -type ListFsJob = GenericFsJob & { type: "list" }; -type ReadFsJob = GenericFsJob & { type: "read" }; -type WriteFsJob = GenericFsJob & { type: "write"; contents: string }; -type DeleteFsJob = GenericFsJob & { type: "delete" }; +export type InitializeFsJob = GenericFsJob & { type: "initialize" }; +type ListFsJob = GenericFsJob & { type: "list"; filename: string }; +type ReadFsJob = GenericFsJob & { type: "read"; filename: string }; +type WriteFsJob = GenericFsJob & { type: "write"; filename: string; contents: T }; +type DeleteFsJob = GenericFsJob & { type: "delete"; filename: string }; -export type FsJob = ListFsJob | ReadFsJob | WriteFsJob | DeleteFsJob; -type FsJobResult = string | string[] | void; +type OpenExternalFsJob = GenericFsJob & { type: "open-external"; extension: string }; +type SaveExternalFsJob = GenericFsJob & { type: "save-external"; filename: string; contents: T }; -export class FsJobHandler { +export type FsJob = + | InitializeFsJob + | ListFsJob + | ReadFsJob + | WriteFsJob + | DeleteFsJob + | OpenExternalFsJob + | SaveExternalFsJob; +type FsJobResult = T | string[] | void; + +export class FsJobHandler { readonly rootDir: string; + private readonly storage: StorageInterface; + private initialized = false; - constructor(subDir: string) { + constructor(subDir: string, storage: StorageInterface) { this.rootDir = path.join(userData, subDir); + this.storage = storage; } - handleJob(job: FsJob): Promise { + async initialize(): Promise { + if (this.initialized) { + return; + } + + // Create the directory so that users know where to put files + await fs.mkdir(this.rootDir, { recursive: true }); + this.initialized = true; + } + + handleJob(job: FsJob): Promise> { + switch (job.type) { + case "initialize": + return this.initialize(); + case "open-external": + return this.openExternal(job.extension); + case "save-external": + return this.saveExternal(job.filename, job.contents); + } + const filename = this.safeFileName(job.filename); switch (job.type) { case "list": return this.list(filename); case "read": - return this.read(filename); + return this.storage.read(filename); case "write": return this.write(filename, job.contents); case "delete": - return this.delete(filename); + return this.storage.delete(filename); } // @ts-expect-error this method can actually receive garbage throw new Error(`Unknown FS job type: ${job.type}`); } + private async openExternal(extension: string): Promise { + const filters = this.getFileDialogFilters(extension === "*" ? undefined : extension); + const window = BrowserWindow.getAllWindows()[0]!; + + const result = await dialog.showOpenDialog(window, { filters, properties: ["openFile"] }); + if (result.canceled) { + return undefined; + } + + return await this.storage.read(result.filePaths[0]); + } + + private async saveExternal(filename: string, contents: T): Promise { + // Try to guess extension + const ext = filename.indexOf(".") < 1 ? filename.split(".").at(-1)! : undefined; + const filters = this.getFileDialogFilters(ext); + const window = BrowserWindow.getAllWindows()[0]!; + + const result = await dialog.showSaveDialog(window, { defaultPath: filename, filters }); + if (result.canceled) { + return; + } + + return await this.storage.write(result.filePath, contents); + } + + private getFileDialogFilters(extension?: string): FileFilter[] { + const filters: FileFilter[] = [{ name: "All files", extensions: ["*"] }]; + + if (extension !== undefined) { + filters.unshift({ + name: `${extension.toUpperCase()} files`, + extensions: [extension], + }); + } + + return filters; + } + private list(subdir: string): Promise { // Bare-bones implementation return fs.readdir(subdir); } - private read(file: string): Promise { - return fs.readFile(file, "utf-8"); - } - - private async write(file: string, contents: string): Promise { + private async write(file: string, contents: T): Promise { // The target directory might not exist, ensure it does const parentDir = path.dirname(file); await fs.mkdir(parentDir, { recursive: true }); - // Backups not implemented yet. - await fs.writeFile(file, contents, { - encoding: "utf-8", - flush: true, - }); - return contents; - } - - private delete(file: string): Promise { - return fs.unlink(file); + await this.storage.write(file, contents); } private safeFileName(name: string) { diff --git a/electron/src/index.ts b/electron/src/index.ts index 8522b4f0..884ed847 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -1,7 +1,6 @@ import { BrowserWindow, app, shell } from "electron"; import path from "path"; import { defaultWindowTitle, pageUrl, switches } from "./config.js"; -import { FsJobHandler } from "./fsjob.js"; import { IpcHandler } from "./ipc.js"; import { ModLoader } from "./mods/loader.js"; import { ModProtocolHandler } from "./mods/protocol_handler.js"; @@ -20,15 +19,9 @@ if (!app.requestSingleInstanceLock()) { }); } -// TODO: Implement a redirector/advanced storage system -// Let mods have own data directories with easy access and -// split savegames/configs - only implement backups and gzip -// files if requested. Perhaps, use streaming to make large -// transfers less "blocking" -const fsJob = new FsJobHandler("saves"); const modLoader = new ModLoader(); const modProtocol = new ModProtocolHandler(modLoader); -const ipc = new IpcHandler(fsJob, modLoader); +const ipc = new IpcHandler(modLoader); function createWindow() { // The protocol can only be handled after "ready" event diff --git a/electron/src/ipc.ts b/electron/src/ipc.ts index 43a6b0d4..d44c972e 100644 --- a/electron/src/ipc.ts +++ b/electron/src/ipc.ts @@ -1,13 +1,13 @@ import { BrowserWindow, IpcMainInvokeEvent, ipcMain } from "electron"; import { FsJob, FsJobHandler } from "./fsjob.js"; import { ModLoader } from "./mods/loader.js"; +import { SavesStorage } from "./storage/saves.js"; export class IpcHandler { - private readonly fsJob: FsJobHandler; + private readonly savesHandler = new FsJobHandler("saves", new SavesStorage()); private readonly modLoader: ModLoader; - constructor(fsJob: FsJobHandler, modLoader: ModLoader) { - this.fsJob = fsJob; + constructor(modLoader: ModLoader) { this.modLoader = modLoader; } @@ -20,8 +20,12 @@ export class IpcHandler { // ipcMain.handle("open-mods-folder", ...) } - private handleFsJob(_event: IpcMainInvokeEvent, job: FsJob) { - return this.fsJob.handleJob(job); + private handleFsJob(_event: IpcMainInvokeEvent, job: FsJob) { + if (job.id !== "saves") { + throw new Error("Storages other than saves/ are not implemented yet"); + } + + return this.savesHandler.handleJob(job); } private async getMods() { diff --git a/electron/src/storage/interface.ts b/electron/src/storage/interface.ts new file mode 100644 index 00000000..f0e59804 --- /dev/null +++ b/electron/src/storage/interface.ts @@ -0,0 +1,5 @@ +export interface StorageInterface { + read(file: string): Promise; + write(file: string, contents: T): Promise; + delete(file: string): Promise; +} diff --git a/electron/src/storage/raw.ts b/electron/src/storage/raw.ts new file mode 100644 index 00000000..0d6a1a56 --- /dev/null +++ b/electron/src/storage/raw.ts @@ -0,0 +1,16 @@ +import fs from "node:fs/promises"; +import { StorageInterface } from "./interface.js"; + +export class RawStorage implements StorageInterface { + read(file: string): Promise { + return fs.readFile(file, "utf-8"); + } + + write(file: string, contents: string): Promise { + return fs.writeFile(file, contents, "utf-8"); + } + + delete(file: string): Promise { + return fs.unlink(file); + } +} diff --git a/electron/src/storage/saves.ts b/electron/src/storage/saves.ts new file mode 100644 index 00000000..750bd571 --- /dev/null +++ b/electron/src/storage/saves.ts @@ -0,0 +1,54 @@ +import { decodeAsync, encode } from "@msgpack/msgpack"; +import fs from "node:fs"; +import { pipeline } from "node:stream/promises"; +import { createGunzip, createGzip } from "node:zlib"; +import { StorageInterface } from "./interface.js"; + +/** + * This storage implementation is used for savegame files and other + * ReadWriteProxy objects. It uses gzipped MessagePack as the file format. + */ +export class SavesStorage implements StorageInterface { + async read(file: string): Promise { + const stream = fs.createReadStream(file); + const gunzip = createGunzip(); + + try { + // Any filesystem errors will be uncovered here. This code ensures we return the most + // relevant rejection, or resolve with the decoded data + const [readResult, decodeResult] = await Promise.allSettled([ + pipeline(stream, gunzip), + decodeAsync(gunzip), + ]); + + if (decodeResult.status === "fulfilled") { + return decodeResult.value; + } + + // Return the most relevant error + throw readResult.status === "rejected" ? readResult.reason : decodeResult.reason; + } finally { + stream.close(); + gunzip.close(); + } + } + + async write(file: string, contents: unknown): Promise { + const stream = fs.createWriteStream(file); + const gzip = createGzip(); + + try { + const encoded = encode(contents); + const blob = new Blob([encoded]); + + return await pipeline(blob.stream(), gzip, stream); + } finally { + gzip.close(); + stream.close(); + } + } + + delete(file: string): Promise { + return fs.promises.unlink(file); + } +} diff --git a/src/js/application.js b/src/js/application.js index b61eb923..4a0bc895 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -13,7 +13,7 @@ import { MOD_SIGNALS } from "./mods/mod_signals"; import { MODS } from "./mods/modloader"; import { ClientAPI } from "./platform/api"; import { Sound } from "./platform/sound"; -import { Storage } from "./platform/storage"; +import { Storage, STORAGE_SAVES } from "./platform/storage"; import { PlatformWrapperImplElectron } from "./platform/wrapper"; import { ApplicationSettings } from "./profile/application_settings"; import { SavegameManager } from "./savegame/savegame_manager"; @@ -55,7 +55,9 @@ export class Application { this.unloaded = false; // Platform stuff - this.storage = new Storage(this); + this.storage = new Storage(this, STORAGE_SAVES); + await this.storage.initialize(); + this.platformWrapper = new PlatformWrapperImplElectron(this); // Global stuff diff --git a/src/js/core/async_compression.js b/src/js/core/async_compression.js deleted file mode 100644 index ea5177e5..00000000 --- a/src/js/core/async_compression.js +++ /dev/null @@ -1,101 +0,0 @@ -// @ts-expect-error FIXME: missing typings -import CompressionWorker from "../webworkers/compression.worker"; - -import { createLogger } from "./logging"; -import { round2Digits } from "./utils"; - -const logger = createLogger("async_compression"); - -export const compressionPrefix = String.fromCodePoint(1); - -/** - * @typedef {{ - * errorHandler: function(any) : void, - * resolver: function(any) : void, - * startTime: number - * }} JobEntry - */ - -class AsynCompression { - constructor() { - this.worker = new CompressionWorker(); - - this.currentJobId = 1000; - - /** @type {Object.} */ - this.currentJobs = {}; - - this.worker.addEventListener("message", event => { - const { jobId, result } = event.data; - const jobData = this.currentJobs[jobId]; - if (!jobData) { - logger.error("Failed to resolve job result, job id", jobId, "is not known"); - return; - } - - const duration = performance.now() - jobData.startTime; - logger.log( - "Got job", - jobId, - "response within", - round2Digits(duration), - "ms: ", - result.length, - "bytes" - ); - const resolver = jobData.resolver; - delete this.currentJobs[jobId]; - resolver(result); - }); - - this.worker.addEventListener("error", err => { - logger.error("Got error from webworker:", err, "aborting all jobs"); - const failureCalls = []; - for (const jobId in this.currentJobs) { - failureCalls.push(this.currentJobs[jobId].errorHandler); - } - this.currentJobs = {}; - for (let i = 0; i < failureCalls.length; ++i) { - failureCalls[i](err); - } - }); - } - - /** - * Compresses any object - * @param {any} obj - */ - compressObjectAsync(obj) { - logger.log("Compressing object async (optimized)"); - return this.internalQueueJob("compressObject", { - obj, - compressionPrefix, - }); - } - - /** - * Queues a new job - * @param {string} job - * @param {any} data - * @returns {Promise} - */ - internalQueueJob(job, data) { - const jobId = ++this.currentJobId; - return new Promise((resolve, reject) => { - const errorHandler = err => { - logger.error("Failed to compress job", jobId, ":", err); - reject(err); - }; - this.currentJobs[jobId] = { - errorHandler, - resolver: resolve, - startTime: performance.now(), - }; - - logger.log("Posting job", job, "/", jobId); - this.worker.postMessage({ jobId, job, data }); - }); - } -} - -export const asyncCompressor = new AsynCompression(); diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index a7f03d52..23770f4d 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -3,12 +3,9 @@ import { Storage } from "@/platform/storage"; /* typehints:end */ import { FsError } from "@/platform/fs_error"; -import { asyncCompressor, compressionPrefix } from "./async_compression"; import { IS_DEBUG, globalConfig } from "./config"; import { ExplainedResult } from "./explained_result"; import { createLogger } from "./logging"; -import { compressX64, decompressX64 } from "./lzstring"; -import { computeCrc } from "./sensitive_utils.encrypt"; import debounce from "debounce-promise"; @@ -84,9 +81,8 @@ export class ReadWriteProxy { * @param {object} obj */ static serializeObject(obj) { - const jsonString = JSON.stringify(obj); - const checksum = computeCrc(jsonString + salt); - return compressionPrefix + compressX64(checksum + jsonString); + // TODO: Remove redundant method + return obj; } /** @@ -94,29 +90,8 @@ export class ReadWriteProxy { * @param {object} text */ static deserializeObject(text) { - const decompressed = decompressX64(text.substr(compressionPrefix.length)); - if (!decompressed) { - // LZ string decompression failure - throw new Error("bad-content / decompression-failed"); - } - if (decompressed.length < 40) { - // String too short - throw new Error("bad-content / payload-too-small"); - } - - // Compare stored checksum with actual checksum - const checksum = decompressed.substring(0, 40); - const jsonString = decompressed.substr(40); - - const desiredChecksum = computeCrc(jsonString + salt); - - if (desiredChecksum !== checksum) { - // Checksum mismatch - throw new Error("bad-content / checksum-mismatch"); - } - - const parsed = JSON.parse(jsonString); - return parsed; + // TODO: Remove redundant method + return text; } /** @@ -140,11 +115,8 @@ export class ReadWriteProxy { * @returns {Promise} */ doWriteAsync() { - return asyncCompressor - .compressObjectAsync(this.currentData) - .then(compressed => { - return this.storage.writeFileAsync(this.filename, compressed); - }) + return this.storage + .writeFileAsync(this.filename, this.currentData) .then(() => { logger.log("📄 Wrote", this.filename); }) @@ -178,59 +150,12 @@ export class ReadWriteProxy { .then(rawData => { if (rawData == null) { // So, the file has not been found, use default data - return JSON.stringify(this.getDefaultData()); + return this.getDefaultData(); } - if (rawData.startsWith(compressionPrefix)) { - const decompressed = decompressX64(rawData.substr(compressionPrefix.length)); - if (!decompressed) { - // LZ string decompression failure - return Promise.reject("bad-content / decompression-failed"); - } - if (decompressed.length < 40) { - // String too short - return Promise.reject("bad-content / payload-too-small"); - } - - // Compare stored checksum with actual checksum - const checksum = decompressed.substring(0, 40); - const jsonString = decompressed.slice(40); - - const desiredChecksum = computeCrc(jsonString + salt); - - if (desiredChecksum !== checksum) { - // Checksum mismatch - return Promise.reject( - "bad-content / checksum-mismatch: " + desiredChecksum + " vs " + checksum - ); - } - return jsonString; - } else { - if (!G_IS_DEV) { - return Promise.reject("bad-content / missing-compression"); - } - } return rawData; }) - // Parse JSON, this could throw but that's fine - .then(res => { - try { - return JSON.parse(res); - } catch (ex) { - logger.error( - "Failed to parse file content of", - this.filename, - ":", - ex, - "(content was:", - res, - ")" - ); - throw new Error("invalid-serialized-data"); - } - }) - // Verify basic structure .then(contents => { const result = this.internalVerifyBasicStructure(contents); diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 291b5a42..a0e740bd 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -599,38 +599,6 @@ export function fillInLinkIntoTranslation(translation, link) { .replace("", ""); } -/** - * Generates a file download - * @param {string} filename - * @param {string} text - */ -export function generateFileDownload(filename, text) { - const element = document.createElement("a"); - element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); - element.setAttribute("download", filename); - - element.style.display = "none"; - document.body.appendChild(element); - - element.click(); - document.body.removeChild(element); -} - -/** - * Starts a file chooser - * @param {string} acceptedType - */ -export function startFileChoose(acceptedType = ".bin") { - const input = document.createElement("input"); - input.type = "file"; - input.accept = acceptedType; - - return new Promise(resolve => { - input.onchange = _ => resolve(input.files[0]); - input.click(); - }); -} - const MAX_ROMAN_NUMBER = 49; const romanLiteralsCache = ["0"]; diff --git a/src/js/platform/storage.ts b/src/js/platform/storage.ts index 250a0ee2..d947dc33 100644 --- a/src/js/platform/storage.ts +++ b/src/js/platform/storage.ts @@ -1,53 +1,68 @@ import { Application } from "@/application"; import { FsError } from "./fs_error"; +export const STORAGE_SAVES = "saves"; +export const STORAGE_MOD_PREFIX = "mod/"; + export class Storage { readonly app: Application; + readonly id: string; - constructor(app: Application) { + constructor(app: Application, id: string) { this.app = app; + this.id = id; } /** * Initializes the storage */ initialize(): Promise { - return Promise.resolve(); + return this.invokeFsJob({ type: "initialize" }); } /** * Writes a string to a file asynchronously */ - writeFileAsync(filename: string, contents: string): Promise { - return ipcRenderer - .invoke("fs-job", { - type: "write", - filename, - contents, - }) - .catch(e => this.wrapError(e)); + writeFileAsync(filename: string, contents: unknown): Promise { + return this.invokeFsJob({ type: "write", filename, contents }); } /** * Reads a string asynchronously */ - readFileAsync(filename: string): Promise { - return ipcRenderer - .invoke("fs-job", { - type: "read", - filename, - }) - .catch(e => this.wrapError(e)); + readFileAsync(filename: string): Promise { + return this.invokeFsJob({ type: "read", filename }); } /** * Tries to delete a file */ deleteFileAsync(filename: string): Promise { + return this.invokeFsJob({ type: "delete", filename }); + } + + /** + * Displays the "Open File" dialog to let user pick a file. Returns the + * decompressed file contents, or undefined if the operation was canceled + */ + requestOpenFile(extension: string): Promise { + return this.invokeFsJob({ type: "open-external", extension }); + } + + /** + * Displays the "Save File" dialog to let user pick a file. If the user + * picks a file, the passed contents will be compressed and written to + * that file. + */ + requestSaveFile(filename: string, contents: unknown): Promise { + return this.invokeFsJob({ type: "save-external", filename, contents }); + } + + private invokeFsJob(data: object) { return ipcRenderer .invoke("fs-job", { - type: "delete", - filename, + id: this.id, + ...data, }) .catch(e => this.wrapError(e)); } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index b4054553..37dc4772 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -6,16 +6,6 @@ import { MODS } from "../mods/modloader"; import { BaseSavegameInterface } from "./savegame_interface"; import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry"; import { SavegameSerializer } from "./savegame_serializer"; -import { SavegameInterface_V1001 } from "./schemas/1001"; -import { SavegameInterface_V1002 } from "./schemas/1002"; -import { SavegameInterface_V1003 } from "./schemas/1003"; -import { SavegameInterface_V1004 } from "./schemas/1004"; -import { SavegameInterface_V1005 } from "./schemas/1005"; -import { SavegameInterface_V1006 } from "./schemas/1006"; -import { SavegameInterface_V1007 } from "./schemas/1007"; -import { SavegameInterface_V1008 } from "./schemas/1008"; -import { SavegameInterface_V1009 } from "./schemas/1009"; -import { SavegameInterface_V1010 } from "./schemas/1010"; const logger = createLogger("savegame"); @@ -112,58 +102,8 @@ export class Savegame extends ReadWriteProxy { * @param {SavegameData} data */ migrate(data) { - if (data.version < 1000) { - return ExplainedResult.bad("Can not migrate savegame, too old"); - } - - if (data.version === 1000) { - SavegameInterface_V1001.migrate1000to1001(data); - data.version = 1001; - } - - if (data.version === 1001) { - SavegameInterface_V1002.migrate1001to1002(data); - data.version = 1002; - } - - if (data.version === 1002) { - SavegameInterface_V1003.migrate1002to1003(data); - data.version = 1003; - } - - if (data.version === 1003) { - SavegameInterface_V1004.migrate1003to1004(data); - data.version = 1004; - } - - if (data.version === 1004) { - SavegameInterface_V1005.migrate1004to1005(data); - data.version = 1005; - } - - if (data.version === 1005) { - SavegameInterface_V1006.migrate1005to1006(data); - data.version = 1006; - } - - if (data.version === 1006) { - SavegameInterface_V1007.migrate1006to1007(data); - data.version = 1007; - } - - if (data.version === 1007) { - SavegameInterface_V1008.migrate1007to1008(data); - data.version = 1008; - } - - if (data.version === 1008) { - SavegameInterface_V1009.migrate1008to1009(data); - data.version = 1009; - } - - if (data.version === 1009) { - SavegameInterface_V1010.migrate1009to1010(data); - data.version = 1010; + if (data.version !== this.getCurrentVersion()) { + return ExplainedResult.bad("Savegame upgrade is not supported"); } return ExplainedResult.good(); diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index e34c59e4..4d26c069 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -2,16 +2,13 @@ import { globalConfig, THIRDPARTY_URLS } from "../core/config"; import { GameState } from "../core/game_state"; import { DialogWithForm } from "../core/modal_dialog_elements"; import { FormElementInput } from "../core/modal_dialog_forms"; -import { ReadWriteProxy } from "../core/read_write_proxy"; import { formatSecondsToTimeAgo, - generateFileDownload, getLogoSprite, makeButton, makeDiv, makeDivElement, removeAllChildren, - startFileChoose, waitNextFrame, } from "../core/utils"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; @@ -137,58 +134,34 @@ export class MainMenuState extends GameState { /** * Asks the user to import a savegame */ - requestImportSavegame() { - // Create a 'fake' file-input to accept savegames - startFileChoose(".bin").then(file => { - if (file) { - const closeLoader = this.dialogs.showLoadingDialog(); - waitNextFrame().then(() => { - const reader = new FileReader(); - reader.addEventListener("load", event => { - const contents = event.target.result; - let realContent; + async requestImportSavegame() { + const closeLoader = this.dialogs.showLoadingDialog(); + await waitNextFrame(); - try { - realContent = ReadWriteProxy.deserializeObject(contents); - } catch (err) { - closeLoader(); - this.dialogs.showWarning( - T.dialogs.importSavegameError.title, - T.dialogs.importSavegameError.text + "

" + err - ); - return; - } + const data = await this.app.storage.requestOpenFile("bin"); + if (data === undefined) { + // User canceled the request + closeLoader(); + return; + } - this.app.savegameMgr.importSavegame(realContent).then( - () => { - closeLoader(); - this.dialogs.showWarning( - T.dialogs.importSavegameSuccess.title, - T.dialogs.importSavegameSuccess.text - ); + try { + this.app.savegameMgr.importSavegame(data); + closeLoader(); + this.dialogs.showWarning( + T.dialogs.importSavegameSuccess.title, + T.dialogs.importSavegameSuccess.text + ); - this.renderMainMenu(); - this.renderSavegames(); - }, - err => { - closeLoader(); - this.dialogs.showWarning( - T.dialogs.importSavegameError.title, - T.dialogs.importSavegameError.text + ":

" + err - ); - } - ); - }); - reader.addEventListener("error", error => { - this.dialogs.showWarning( - T.dialogs.importSavegameError.title, - T.dialogs.importSavegameError.text + ":

" + error - ); - }); - reader.readAsText(file, "utf-8"); - }); - } - }); + this.renderMainMenu(); + this.renderSavegames(); + } catch (err) { + closeLoader(); + this.dialogs.showWarning( + T.dialogs.importSavegameError.title, + T.dialogs.importSavegameError.text + ":

" + err + ); + } } onBackButton() { @@ -602,9 +575,8 @@ export class MainMenuState extends GameState { downloadGame(game) { const savegame = this.app.savegameMgr.getSavegameById(game.internalId); savegame.readAsync().then(() => { - const data = ReadWriteProxy.serializeObject(savegame.currentData); const filename = (game.name || "unnamed") + ".bin"; - generateFileDownload(filename, data); + savegame.storage.requestSaveFile(filename, savegame.currentData); }); } diff --git a/src/js/webworkers/compression.worker.js b/src/js/webworkers/compression.worker.js deleted file mode 100644 index 83862d10..00000000 --- a/src/js/webworkers/compression.worker.js +++ /dev/null @@ -1,29 +0,0 @@ -import { globalConfig } from "../core/config"; -import { compressX64 } from "../core/lzstring"; -import { computeCrc } from "../core/sensitive_utils.encrypt"; - -self.addEventListener("message", event => { - // @ts-ignore - const { jobId, job, data } = event.data; - const result = performJob(job, data); - - // @ts-ignore - self.postMessage({ jobId, result }); -}); - -function performJob(job, data) { - switch (job) { - case "compressX64": { - return compressX64(data); - } - - case "compressObject": { - const stringified = JSON.stringify(data.obj); - - const checksum = computeCrc(stringified + globalConfig.info.file); - return data.compressionPrefix + compressX64(checksum + stringified); - } - default: - throw new Error("Webworker: Unknown job: " + job); - } -} From 6e81afd372978a223b197df88b495260e9cd369e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=97=D0=BB=20=D0=93=D1=80=D0=B8?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=27=D1=94=D0=B2?= Date: Fri, 18 Apr 2025 11:09:45 +0300 Subject: [PATCH 4/4] Remove some compression leftovers Remove the CRC dependency and "binary file salt" in globalConfig. --- package-lock.json | 13 +++---------- package.json | 1 - src/js/core/config.ts | 6 ------ src/js/core/read_write_proxy.js | 17 ++--------------- src/js/core/sensitive_utils.encrypt.js | 12 ------------ 5 files changed, 5 insertions(+), 44 deletions(-) delete mode 100644 src/js/core/sensitive_utils.encrypt.js diff --git a/package-lock.json b/package-lock.json index b037c9f8..4a0c7a90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "ajv": "^6.10.2", "circular-json": "^0.5.9", "clipboard-copy": "^3.1.0", - "crc": "^3.8.0", "debounce-promise": "^3.1.2", "howler": "^2.1.2", "lz-string": "^1.4.4" @@ -2434,6 +2433,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -3253,6 +3253,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -4363,15 +4364,6 @@ "node": ">=4" } }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "license": "MIT", - "dependencies": { - "buffer": "^5.1.0" - } - }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -9937,6 +9929,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", diff --git a/package.json b/package.json index f4dfee1a..2e8ae979 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "ajv": "^6.10.2", "circular-json": "^0.5.9", "clipboard-copy": "^3.1.0", - "crc": "^3.8.0", "debounce-promise": "^3.1.2", "howler": "^2.1.2", "lz-string": "^1.4.4" diff --git a/src/js/core/config.ts b/src/js/core/config.ts index dcff1710..6ec9609b 100644 --- a/src/js/core/config.ts +++ b/src/js/core/config.ts @@ -110,12 +110,6 @@ export const globalConfig = { rendering: {}, debug, - - // Secret vars - info: { - // Binary file salt - file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=", - }, }; export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 23770f4d..9bf3fe97 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -3,7 +3,7 @@ import { Storage } from "@/platform/storage"; /* typehints:end */ import { FsError } from "@/platform/fs_error"; -import { IS_DEBUG, globalConfig } from "./config"; +import { IS_DEBUG } from "./config"; import { ExplainedResult } from "./explained_result"; import { createLogger } from "./logging"; @@ -11,8 +11,6 @@ import debounce from "debounce-promise"; const logger = createLogger("read_write_proxy"); -const salt = globalConfig.info.file; - // Helper which only writes / reads if verify() works. Also performs migration export class ReadWriteProxy { constructor(storage, filename) { @@ -139,23 +137,12 @@ export class ReadWriteProxy { logger.log("File not found, using default data"); // File not found or unreadable, assume default file - return Promise.resolve(null); + return Promise.resolve(this.getDefaultData()); } return Promise.reject("file-error: " + err); }) - // Decrypt data (if its encrypted) - // @ts-ignore - .then(rawData => { - if (rawData == null) { - // So, the file has not been found, use default data - return this.getDefaultData(); - } - - return rawData; - }) - // Verify basic structure .then(contents => { const result = this.internalVerifyBasicStructure(contents); diff --git a/src/js/core/sensitive_utils.encrypt.js b/src/js/core/sensitive_utils.encrypt.js deleted file mode 100644 index e61e0b67..00000000 --- a/src/js/core/sensitive_utils.encrypt.js +++ /dev/null @@ -1,12 +0,0 @@ -import crc32 from "crc/crc32"; - -// Distinguish legacy crc prefixes -export const CRC_PREFIX = "crc32".padEnd(32, "-"); - -/** - * Computes the crc for a given string - * @param {string} str - */ -export function computeCrc(str) { - return CRC_PREFIX + crc32(str).toString(16).padStart(8, "0"); -}