From 8c4fcfe04532537b01998d9417441f9fbb880f6c Mon Sep 17 00:00:00 2001 From: catloversg <152669316+catloversg@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:47:02 +0700 Subject: [PATCH] CODEBASE: Recheck all usages of typecasting with JSON.parse (#1775) --- src/CotMG/Helper.tsx | 30 ++++++++++++++++++++++-------- src/Player.ts | 5 +++++ src/RemoteFileAPI/Remote.ts | 4 ++++ src/SaveObject.ts | 23 +++++++++++++---------- src/Settings/Settings.ts | 9 +++------ src/utils/helpers/typeAssertion.ts | 4 +++- test/jest/FullSave.test.ts | 8 +++++--- 7 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/CotMG/Helper.tsx b/src/CotMG/Helper.tsx index 0c45899d8..12227c791 100644 --- a/src/CotMG/Helper.tsx +++ b/src/CotMG/Helper.tsx @@ -1,3 +1,4 @@ +import { dialogBoxCreate } from "../ui/React/DialogBox"; import { Reviver } from "../utils/JSONReviver"; import { BaseGift } from "./BaseGift"; @@ -6,15 +7,26 @@ import { StaneksGift } from "./StaneksGift"; export let staneksGift = new StaneksGift(); export function loadStaneksGift(saveString: string): void { - if (saveString) { - staneksGift = JSON.parse(saveString, Reviver) as StaneksGift; - } else { + let staneksGiftData: unknown; + try { + staneksGiftData = JSON.parse(saveString, Reviver); + if (!(staneksGiftData instanceof StaneksGift)) { + throw new Error(`Data of Stanek's Gift is not an instance of "StaneksGift"`); + } + } catch (error) { + console.error(error); + console.error("Invalid StaneksGiftSave:", saveString); staneksGift = new StaneksGift(); + setTimeout(() => { + dialogBoxCreate(`Cannot load data of Stanek's Gift. Stanek's Gift is reset. Error: ${error}.`); + }, 1000); + return; } + staneksGift = staneksGiftData; } export function zeros(width: number, height: number): number[][] { - const array: number[][] = []; + const array = []; for (let i = 0; i < width; ++i) { array.push(Array(height).fill(0)); @@ -24,14 +36,16 @@ export function zeros(width: number, height: number): number[][] { } export function calculateGrid(gift: BaseGift): number[][] { - const newgrid = zeros(gift.width(), gift.height()) as unknown as number[][]; + const newGrid = zeros(gift.width(), gift.height()); for (let i = 0; i < gift.width(); i++) { for (let j = 0; j < gift.height(); j++) { const fragment = gift.fragmentAt(i, j); - if (!fragment) continue; - newgrid[i][j] = 1; + if (!fragment) { + continue; + } + newGrid[i][j] = 1; } } - return newgrid; + return newGrid; } diff --git a/src/Player.ts b/src/Player.ts index 9c82bbdef..1deb455c5 100644 --- a/src/Player.ts +++ b/src/Player.ts @@ -11,6 +11,11 @@ export function setPlayer(playerObj: PlayerObject): void { } export function loadPlayer(saveString: string): PlayerObject { + /** + * If we want to check player with "instanceof PlayerObject", we have to import PlayerObject normally (not "import + * type"). It will create a cyclic dependency. Fixing this cyclic dependency is really hard. It's not worth the + * effort, so we typecast it here. + */ const player = JSON.parse(saveString, Reviver) as PlayerObject; player.money = parseFloat(player.money + ""); player.exploits = sanitizeExploits(player.exploits); diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts index f9b9b8d28..4a69484fd 100644 --- a/src/RemoteFileAPI/Remote.ts +++ b/src/RemoteFileAPI/Remote.ts @@ -40,6 +40,10 @@ export class Remote { } function handleMessageEvent(this: WebSocket, e: MessageEvent): void { + /** + * Validating e.data and the result of JSON.parse() is too troublesome, so we typecast them here. If the data is + * invalid, it means the RFA "client" (the tool that the player is using) is buggy, but that's not our problem. + */ const msg = JSON.parse(e.data as string) as RFAMessage; if (!msg.method || !RFARequestHandler[msg.method]) { diff --git a/src/SaveObject.ts b/src/SaveObject.ts index d5778cde8..cf1202254 100644 --- a/src/SaveObject.ts +++ b/src/SaveObject.ts @@ -45,6 +45,7 @@ import { downloadContentAsFile } from "./utils/FileUtils"; import { showAPIBreaks } from "./utils/APIBreaks/APIBreak"; import { breakInfos261 } from "./utils/APIBreaks/2.6.1"; import { handleGetSaveDataInfoError } from "./Netscript/ErrorMessages"; +import { isObject } from "./utils/helpers/typeAssertion"; /* SaveObject.js * Defines the object used to save/load games @@ -220,23 +221,25 @@ class BitburnerSaveObject { } if (!decodedSaveData || decodedSaveData === "") { - return Promise.reject(new Error("Save game is invalid")); + console.error("decodedSaveData:", decodedSaveData); + return Promise.reject(new Error("Save game is invalid. The save data cannot be decoded.")); } - let parsedSaveData; + let parsedSaveData: unknown; try { - parsedSaveData = JSON.parse(decodedSaveData) as { - ctor: string; - data: { - PlayerSave: string; - }; - }; + parsedSaveData = JSON.parse(decodedSaveData); } catch (error) { console.error(error); // We'll handle below } - if (!parsedSaveData || parsedSaveData.ctor !== "BitburnerSaveObject" || !parsedSaveData.data) { - return Promise.reject(new Error("Save game did not seem valid")); + if ( + !isObject(parsedSaveData) || + parsedSaveData.ctor !== "BitburnerSaveObject" || + !isObject(parsedSaveData.data) || + typeof parsedSaveData.data.PlayerSave !== "string" + ) { + console.error("decodedSaveData:", decodedSaveData); + return Promise.reject(new Error("Save game is invalid. The decoded save data is not valid.")); } const data: ImportData = { diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index c90077dab..4cc9e8b56 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -3,6 +3,7 @@ import { defaultTheme } from "../Themes/Themes"; import { defaultStyles } from "../Themes/Styles"; import { CursorStyle, CursorBlinking, WordWrapOptions } from "../ScriptEditor/ui/Options"; import { defaultMonacoTheme } from "../ScriptEditor/ui/themes"; +import { objectAssert } from "../utils/helpers/typeAssertion"; /** * This function won't be able to catch **all** invalid hostnames, and it's still fine. In order to validate a hostname @@ -157,12 +158,8 @@ export const Settings = { disableSuffixes: false, load(saveString: string) { - const save = JSON.parse(saveString) as { - theme?: typeof Settings.theme; - styles?: typeof Settings.styles; - overview?: typeof Settings.overview; - EditorTheme?: typeof Settings.EditorTheme; - }; + const save: unknown = JSON.parse(saveString); + objectAssert(save); save.theme && Object.assign(Settings.theme, save.theme); save.styles && Object.assign(Settings.styles, save.styles); save.overview && Object.assign(Settings.overview, save.overview); diff --git a/src/utils/helpers/typeAssertion.ts b/src/utils/helpers/typeAssertion.ts index 71de52812..80cc19b51 100644 --- a/src/utils/helpers/typeAssertion.ts +++ b/src/utils/helpers/typeAssertion.ts @@ -35,7 +35,9 @@ function getFriendlyType(v: unknown): string { return v === null ? "null" : Array.isArray(v) ? "array" : typeof v; } -//All assertion functions used here should return the friendlyType of the input. +export function isObject(v: unknown): v is Record { + return getFriendlyType(v) === "object"; +} /** For non-objects, and for array/null, throws an error with the friendlyType of v. */ export function objectAssert(v: unknown): asserts v is Record { diff --git a/test/jest/FullSave.test.ts b/test/jest/FullSave.test.ts index 4a0fe0a0b..a01af8713 100644 --- a/test/jest/FullSave.test.ts +++ b/test/jest/FullSave.test.ts @@ -10,13 +10,15 @@ import { Companies } from "../../src/Company/Companies"; describe("Check Save File Continuity", () => { establishInitialConditions(); - // Calling getSaveString forces save info to update - saveObject.getSaveData(); + beforeAll(async () => { + // Calling getSaveString forces save info to update + await saveObject.getSaveData(); + }); const savesToTest = ["FactionsSave", "PlayerSave", "CompaniesSave", "GoSave"] as const; for (const saveToTest of savesToTest) { test(`${saveToTest} continuity`, () => { - const parsed = JSON.parse(saveObject[saveToTest]); + const parsed: unknown = JSON.parse(saveObject[saveToTest]); expect(parsed).toMatchSnapshot(); }); }