mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-21 15:12:06 +02:00
MISC: Harden saving to avoid save data corruption (#2755)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { SaveData } from "./types";
|
||||
import { isSaveData } from "./utils/TypeAssertion";
|
||||
import { InvalidSaveData } from "./utils/SaveDataUtils";
|
||||
import { isSaveData, validateSaveData } from "./utils/TypeAssertion";
|
||||
|
||||
export class IndexedDBVersionError extends Error {
|
||||
constructor(message: string, options: ErrorOptions) {
|
||||
@@ -100,6 +101,12 @@ export async function load(skipCheckingLoadedData = false): Promise<SaveData | u
|
||||
}
|
||||
|
||||
export async function save(saveData: SaveData): Promise<void> {
|
||||
// Validate save data at runtime. Players may accidentally manipulate the prototypes of built-in JS objects and cause
|
||||
// functions in SaveObject.ts to generate invalid save data.
|
||||
const validationResult = validateSaveData(saveData);
|
||||
if (!validationResult.success) {
|
||||
throw new InvalidSaveData(validationResult.message);
|
||||
}
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(["savestring"], "readwrite");
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Factions } from "./Faction/Factions";
|
||||
import { staneksGift } from "./CotMG/Helper";
|
||||
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
|
||||
import { Router } from "./ui/GameRoot";
|
||||
import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
|
||||
import "./PersonObjects/Player/PlayerObject"; // For side-effect of creating Player
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElemen
|
||||
textPropsAsString = JSON.stringify(text.props);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Use the current timestamp as the fallback value.
|
||||
}
|
||||
// Use the current timestamp as the fallback value.
|
||||
if (textPropsAsString == null) {
|
||||
textPropsAsString = Date.now().toString();
|
||||
}
|
||||
return cyrb53(textPropsAsString);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file is imported for side effects only.
|
||||
/* Prevent inadvertantly redefining certain window properties,
|
||||
/* Prevent inadvertently redefining certain window properties,
|
||||
which are known to cause unrecoverable game errors when redefined.
|
||||
The player is able to redefine these properties as writable if desired. */
|
||||
Object.defineProperties(window, {
|
||||
@@ -7,3 +7,18 @@ Object.defineProperties(window, {
|
||||
Object: { writable: false },
|
||||
String: { writable: false },
|
||||
});
|
||||
|
||||
// Prevent accidentally manipulating IndexedDB APIs.
|
||||
Object.freeze(IDBFactory.prototype);
|
||||
Object.freeze(IDBDatabase.prototype);
|
||||
Object.freeze(IDBTransaction.prototype);
|
||||
Object.freeze(IDBObjectStore.prototype);
|
||||
Object.freeze(IDBRequest.prototype);
|
||||
Object.freeze(IDBOpenDBRequest.prototype);
|
||||
if (window.indexedDB) {
|
||||
Object.freeze(window.indexedDB);
|
||||
Object.defineProperty(window, "indexedDB", {
|
||||
value: window.indexedDB,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ async function decompress(binaryData: Uint8Array<ArrayBuffer>): Promise<string>
|
||||
}
|
||||
|
||||
export async function encodeJsonSaveString(jsonSaveString: string): Promise<SaveData> {
|
||||
if (jsonSaveString == null) {
|
||||
throw new InvalidSaveData(`jsonSaveString is ${jsonSaveString}`);
|
||||
}
|
||||
if (typeof jsonSaveString !== "string") {
|
||||
console.error(jsonSaveString);
|
||||
throw new InvalidSaveData(`Type of jsonSaveString is ${typeof jsonSaveString}`);
|
||||
}
|
||||
if (!jsonSaveString.startsWith(`{"ctor":"BitburnerSaveObject"`)) {
|
||||
console.error(jsonSaveString);
|
||||
throw new InvalidSaveData(
|
||||
`Invalid jsonSaveString (doesn't seem to contain a BitburnerSaveObject): ${jsonSaveString.slice(0, 100)}`,
|
||||
);
|
||||
}
|
||||
// Fallback to the base64 format if player's browser does not support Compression Streams API.
|
||||
if (canUseBinaryFormat()) {
|
||||
return await compress(jsonSaveString);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SaveData, Unknownify } from "../types";
|
||||
import type { Result } from "@nsdefs";
|
||||
|
||||
// This function is empty because Unknownify<T> is a typesafe assertion on any object with no runtime checks needed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
@@ -90,12 +91,35 @@ export function assertNumberArray(unknownData: unknown, assertFinite = false): a
|
||||
}
|
||||
}
|
||||
|
||||
export function isSaveData(unknownData: unknown): unknownData is SaveData {
|
||||
if (typeof unknownData === "string") {
|
||||
return true;
|
||||
export function validateSaveData(unknownData: unknown): Result {
|
||||
if (unknownData == null) {
|
||||
return { success: false, message: `Save data is ${unknownData}` };
|
||||
}
|
||||
|
||||
return unknownData instanceof Uint8Array && unknownData.buffer instanceof ArrayBuffer;
|
||||
if (unknownData === "") {
|
||||
return { success: false, message: "Save data is an empty string" };
|
||||
}
|
||||
if (typeof unknownData === "string") {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (!(unknownData instanceof Uint8Array)) {
|
||||
console.error(unknownData);
|
||||
return { success: false, message: "Save data is not an instance of Uint8Array" };
|
||||
}
|
||||
if (unknownData.length === 0) {
|
||||
return { success: false, message: "Save data is an empty Uint8Array" };
|
||||
}
|
||||
if (!(unknownData.buffer instanceof ArrayBuffer)) {
|
||||
console.error(unknownData.buffer);
|
||||
return { success: false, message: "Save data is a Uint8Array, but its buffer is not an ArrayBuffer" };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function isSaveData(unknownData: unknown): unknownData is SaveData {
|
||||
return validateSaveData(unknownData).success;
|
||||
}
|
||||
|
||||
export function assertSaveData(unknownData: unknown): asserts unknownData is SaveData {
|
||||
|
||||
Reference in New Issue
Block a user