Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CODEBASE: Recheck all usages of typecasting with JSON.parse #1775

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions src/CotMG/Helper.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { Reviver } from "../utils/JSONReviver";
import { BaseGift } from "./BaseGift";

Expand All @@ -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);
catloversg marked this conversation as resolved.
Show resolved Hide resolved
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<number>(height).fill(0));
Expand All @@ -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;
}
5 changes: 5 additions & 0 deletions src/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/RemoteFileAPI/Remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
23 changes: 13 additions & 10 deletions src/SaveObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
9 changes: 3 additions & 6 deletions src/Settings/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/utils/helpers/typeAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ function getFriendlyType(v: unknown): string {
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
}

export function isObject(v: unknown): v is Record<string, unknown> {
return getFriendlyType(v) === "object";
}

//All assertion functions used here should return the friendlyType of the input.

/** For non-objects, and for array/null, throws an error with the friendlyType of v. */
Expand Down
8 changes: 5 additions & 3 deletions test/jest/FullSave.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down