From 61ffed9b3a1aff48932632964e38fdae114f255d Mon Sep 17 00:00:00 2001 From: David Walker Date: Thu, 7 Dec 2023 17:34:49 -0800 Subject: [PATCH] BUGFIX: Fix additionalMsec overflow issue (#941) --- src/Netscript/NetscriptHelpers.tsx | 108 ++++++------- src/NetscriptFunctions.ts | 207 +++++++++++------------- src/NetscriptFunctions/Singularity.ts | 2 +- src/NetscriptFunctions/UserInterface.ts | 38 ++++- 4 files changed, 176 insertions(+), 179 deletions(-) diff --git a/src/Netscript/NetscriptHelpers.tsx b/src/Netscript/NetscriptHelpers.tsx index ffd243680..602e98b55 100644 --- a/src/Netscript/NetscriptHelpers.tsx +++ b/src/Netscript/NetscriptHelpers.tsx @@ -7,7 +7,7 @@ import { Player } from "@player"; import { ScriptDeath } from "./ScriptDeath"; import { formatExp, formatMoney, formatRam, formatThreads } from "../ui/formatNumber"; import { ScriptArg } from "./ScriptArg"; -import { BasicHGWOptions, RunningScript as IRunningScript, Person as IPerson, Server as IServer } from "@nsdefs"; +import { RunningScript as IRunningScript, Person as IPerson, Server as IServer } from "@nsdefs"; import { Server } from "../Server/Server"; import { calculateHackingChance, @@ -49,7 +49,7 @@ export const helpers = { argsToString, makeBasicErrorMsg, makeRuntimeErrorMsg, - resolveNetscriptRequestedThreads, + validateHGWOptions, checkEnvFlags, checkSingularityAccess, netscriptDelay, @@ -84,46 +84,18 @@ export interface CompleteRunOptions { export interface CompleteSpawnOptions extends CompleteRunOptions { spawnDelay: PositiveInteger; } +/** HGWOptions with non-optional, type-validated members, for passing between internal functions. */ +export interface CompleteHGWOptions { + threads: PositiveInteger; + stock: boolean; + additionalMsec: number; +} export function assertString(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string { if (typeof v !== "string") throw makeRuntimeErrorMsg(ctx, `${argName} expected to be a string. ${debugType(v)}`, "TYPE"); } -/** Will probably remove the below function in favor of a different approach to object type assertion. - * This method cannot be used to handle optional properties. */ -export function assertObjectType( - ctx: NetscriptContext, - name: string, - obj: unknown, - desiredObject: T, -): asserts obj is T { - if (typeof obj !== "object" || obj === null) { - throw makeRuntimeErrorMsg( - ctx, - `Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`, - "TYPE", - ); - } - for (const [key, val] of Object.entries(desiredObject)) { - if (!Object.hasOwn(obj, key)) { - throw makeRuntimeErrorMsg( - ctx, - `Object provided for argument ${name} is missing required property ${key}.`, - "TYPE", - ); - } - const objVal = (obj as Record)[key]; - if (typeof val !== typeof objVal) { - throw makeRuntimeErrorMsg( - ctx, - `Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`, - "TYPE", - ); - } - } -} - const userFriendlyString = (v: unknown): string => { const clip = (s: string): string => { if (s.length > 15) return s.slice(0, 12) + "..."; @@ -266,7 +238,7 @@ function makeBasicErrorMsg(ws: WorkerScript | ScriptDeath, msg: string, type = " } /** Creates an error message string with a stack trace. */ -function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string { +export function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string { const errstack = new Error().stack; if (errstack === undefined) throw new Error("how did we not throw an error?"); const stack = errstack.split("\n").slice(1); @@ -322,23 +294,44 @@ function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME } } -/** Validate requested number of threads for h/g/w options */ -function resolveNetscriptRequestedThreads(ctx: NetscriptContext, requestedThreads?: number): number { +function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOptions { + const result: CompleteHGWOptions = { + threads: 1 as PositiveInteger, + stock: false, + additionalMsec: 0, + }; + if (opts == null) { + return result; + } + if (typeof opts !== "object") { + throw makeRuntimeErrorMsg(ctx, `BasicHGWOptions must be an object if specified, was ${opts}`); + } + // Safe assertion since threadOrOption type has been narrowed to a non-null object + const options = opts as Unknownify; + result.stock = !!options.stock; + result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0); + if (result.additionalMsec < 0) { + throw makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${options.additionalMsec}`); + } + if (result.additionalMsec > 1e9) { + throw makeRuntimeErrorMsg(ctx, `additionalMsec too large (>1e9), got ${options.additionalMsec}`); + } + const requestedThreads = options.threads; const threads = ctx.workerScript.scriptRef.threads; if (!requestedThreads) { - return isNaN(threads) || threads < 1 ? 1 : threads; + result.threads = (isNaN(threads) || threads < 1 ? 1 : threads) as PositiveInteger; + } else { + const positiveThreads = positiveInteger(ctx, "opts.threads", requestedThreads); + if (positiveThreads > threads) { + throw makeRuntimeErrorMsg( + ctx, + `Too many threads requested by ${ctx.function}. Requested: ${positiveThreads}. Has: ${threads}.`, + ); + } + result.threads = positiveThreads; } - const requestedThreadsAsInt = requestedThreads | 0; - if (isNaN(requestedThreads) || requestedThreadsAsInt < 1) { - throw makeRuntimeErrorMsg(ctx, `Invalid thread count: ${requestedThreads}. Threads must be a positive number.`); - } - if (requestedThreadsAsInt > threads) { - throw makeRuntimeErrorMsg( - ctx, - `Too many threads requested by ${ctx.function}. Requested: ${requestedThreads}. Has: ${threads}.`, - ); - } - return requestedThreadsAsInt; + + return result; } /** Validate singularity access by throwing an error if the player does not have access. */ @@ -472,18 +465,9 @@ function isScriptArgs(args: unknown): args is ScriptArg[] { return Array.isArray(args) && args.every(isScriptArg); } -function hack( - ctx: NetscriptContext, - hostname: string, - manual: boolean, - { threads: requestedThreads, stock, additionalMsec: requestedSec }: BasicHGWOptions = {}, -): Promise { +function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: unknown): Promise { const ws = ctx.workerScript; - const threads = helpers.resolveNetscriptRequestedThreads(ctx, requestedThreads); - const additionalMsec = number(ctx, "opts.additionalMsec", requestedSec ?? 0); - if (additionalMsec < 0) { - throw makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`); - } + const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts); const server = getServer(ctx, hostname); if (!(server instanceof Server)) { throw makeRuntimeErrorMsg(ctx, "Cannot be executed on this server."); diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 86f92a0d5..6cca7b27e 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -51,7 +51,7 @@ import { runScriptFromScript } from "./NetscriptWorker"; import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript"; import { workerScripts } from "./Netscript/WorkerScripts"; import { WorkerScript } from "./Netscript/WorkerScript"; -import { helpers, assertObjectType, wrapUserNode } from "./Netscript/NetscriptHelpers"; +import { helpers, wrapUserNode } from "./Netscript/NetscriptHelpers"; import { formatExp, formatNumberNoSuffix, @@ -78,7 +78,7 @@ import { NetscriptCorporation } from "./NetscriptFunctions/Corporation"; import { NetscriptFormulas } from "./NetscriptFunctions/Formulas"; import { NetscriptStockMarket } from "./NetscriptFunctions/StockMarket"; import { NetscriptGrafting } from "./NetscriptFunctions/Grafting"; -import { NS, RecentScript, BasicHGWOptions, ProcessInfo, NSEnums } from "@nsdefs"; +import { NS, RecentScript, ProcessInfo, NSEnums } from "@nsdefs"; import { NetscriptSingularity } from "./NetscriptFunctions/Singularity"; import { dialogBoxCreate } from "./ui/React/DialogBox"; @@ -154,15 +154,10 @@ export const ns: InternalAPI = { return out; }, hasTorRouter: () => () => Player.hasTorRouter(), - hack: - (ctx) => - (_hostname, opts = {}) => { - const hostname = helpers.string(ctx, "hostname", _hostname); - // TODO 2.2: better type safety rework for functions using assertObjectType, then remove function. - const optsValidator: BasicHGWOptions = {}; - assertObjectType(ctx, "opts", opts, optsValidator); - return helpers.hack(ctx, hostname, false, opts); - }, + hack: (ctx) => (_hostname, opts?) => { + const hostname = helpers.string(ctx, "hostname", _hostname); + return helpers.hack(ctx, hostname, false, opts); + }, hackAnalyzeThreads: (ctx) => (_hostname, _hackAmount) => { const hostname = helpers.string(ctx, "hostname", _hostname); const hackAmount = helpers.number(ctx, "hackAmount", _hackAmount); @@ -252,66 +247,58 @@ export const ns: InternalAPI = { helpers.log(ctx, () => `Sleeping for ${convertTimeMsToTimeElapsedString(time, true)}.`); return new Promise((resolve) => setTimeout(() => resolve(true), time)); }, - grow: - (ctx) => - (_hostname, opts = {}) => { - const hostname = helpers.string(ctx, "hostname", _hostname); - const optsValidator: BasicHGWOptions = {}; - assertObjectType(ctx, "opts", opts, optsValidator); - const threads = helpers.resolveNetscriptRequestedThreads(ctx, opts.threads); - const additionalMsec = helpers.number(ctx, "opts.additionalMsec", opts.additionalMsec ?? 0); - if (additionalMsec < 0) { - throw helpers.makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`); - } + grow: (ctx) => (_hostname, opts?) => { + const hostname = helpers.string(ctx, "hostname", _hostname); + const { threads, stock, additionalMsec } = helpers.validateHGWOptions(ctx, opts); - const server = helpers.getServer(ctx, hostname); - if (!(server instanceof Server)) { - helpers.log(ctx, () => "Cannot be executed on this server."); - return Promise.resolve(0); - } + const server = helpers.getServer(ctx, hostname); + if (!(server instanceof Server)) { + helpers.log(ctx, () => "Cannot be executed on this server."); + return Promise.resolve(0); + } - const host = GetServer(ctx.workerScript.hostname); - if (host === null) { - throw new Error("Workerscript host is null"); - } + const host = GetServer(ctx.workerScript.hostname); + if (host === null) { + throw new Error("Workerscript host is null"); + } - // No root access or skill level too low - const canHack = netscriptCanGrow(server); - if (!canHack.res) { - throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || ""); - } + // No root access or skill level too low + const canHack = netscriptCanGrow(server); + if (!canHack.res) { + throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || ""); + } - const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0; + const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0; + helpers.log( + ctx, + () => + `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString( + growTime * 1000, + true, + )} (t=${formatThreads(threads)}).`, + ); + return helpers.netscriptDelay(ctx, growTime * 1000).then(function () { + const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable; + processSingleServerGrowth(server, threads, host.cpuCores); + const moneyAfter = server.moneyAvailable; + ctx.workerScript.scriptRef.recordGrow(server.hostname, threads); + const expGain = calculateHackingExpGain(server, Player) * threads; + const logGrowPercent = moneyAfter / moneyBefore - 1; helpers.log( ctx, () => - `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString( - growTime * 1000, - true, - )} (t=${formatThreads(threads)}).`, + `Available money on '${server.hostname}' grown by ${formatPercent(logGrowPercent, 6)}. Gained ${formatExp( + expGain, + )} hacking exp (t=${formatThreads(threads)}).`, ); - return helpers.netscriptDelay(ctx, growTime * 1000).then(function () { - const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable; - processSingleServerGrowth(server, threads, host.cpuCores); - const moneyAfter = server.moneyAvailable; - ctx.workerScript.scriptRef.recordGrow(server.hostname, threads); - const expGain = calculateHackingExpGain(server, Player) * threads; - const logGrowPercent = moneyAfter / moneyBefore - 1; - helpers.log( - ctx, - () => - `Available money on '${server.hostname}' grown by ${formatPercent(logGrowPercent, 6)}. Gained ${formatExp( - expGain, - )} hacking exp (t=${formatThreads(threads)}).`, - ); - ctx.workerScript.scriptRef.onlineExpGained += expGain; - Player.gainHackingExp(expGain); - if (opts.stock) { - influenceStockThroughServerGrow(server, moneyAfter - moneyBefore); - } - return Promise.resolve(moneyAfter / moneyBefore); - }); - }, + ctx.workerScript.scriptRef.onlineExpGained += expGain; + Player.gainHackingExp(expGain); + if (stock) { + influenceStockThroughServerGrow(server, moneyAfter - moneyBefore); + } + return Promise.resolve(moneyAfter / moneyBefore); + }); + }, growthAnalyze: (ctx) => (_host, _multiplier, _cores = 1) => { @@ -359,64 +346,56 @@ export const ns: InternalAPI = { return 2 * CONSTANTS.ServerFortifyAmount * threads; }, - weaken: - (ctx) => - async (_hostname, opts = {}) => { - const hostname = helpers.string(ctx, "hostname", _hostname); - const optsValidator: BasicHGWOptions = {}; - assertObjectType(ctx, "opts", opts, optsValidator); - const threads = helpers.resolveNetscriptRequestedThreads(ctx, opts.threads); - const additionalMsec = helpers.number(ctx, "opts.additionalMsec", opts.additionalMsec ?? 0); - if (additionalMsec < 0) { - throw helpers.makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${additionalMsec}`); - } + weaken: (ctx) => async (_hostname, opts?) => { + const hostname = helpers.string(ctx, "hostname", _hostname); + const { threads, additionalMsec } = helpers.validateHGWOptions(ctx, opts); - const server = helpers.getServer(ctx, hostname); - if (!(server instanceof Server)) { - helpers.log(ctx, () => "Cannot be executed on this server."); + const server = helpers.getServer(ctx, hostname); + if (!(server instanceof Server)) { + helpers.log(ctx, () => "Cannot be executed on this server."); + return Promise.resolve(0); + } + + // No root access or skill level too low + const canHack = netscriptCanWeaken(server); + if (!canHack.res) { + throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || ""); + } + + const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0; + helpers.log( + ctx, + () => + `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString( + weakenTime * 1000, + true, + )} (t=${formatThreads(threads)})`, + ); + return helpers.netscriptDelay(ctx, weakenTime * 1000).then(function () { + const host = GetServer(ctx.workerScript.hostname); + if (host === null) { + helpers.log(ctx, () => "Server is null, did it die?"); return Promise.resolve(0); } - - // No root access or skill level too low - const canHack = netscriptCanWeaken(server); - if (!canHack.res) { - throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || ""); - } - - const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0; + const cores = host.cpuCores; + const coreBonus = getCoreBonus(cores); + const weakenAmt = CONSTANTS.ServerWeakenAmount * threads * coreBonus; + server.weaken(weakenAmt); + ctx.workerScript.scriptRef.recordWeaken(server.hostname, threads); + const expGain = calculateHackingExpGain(server, Player) * threads; helpers.log( ctx, () => - `Executing on '${server.hostname}' in ${convertTimeMsToTimeElapsedString( - weakenTime * 1000, - true, - )} (t=${formatThreads(threads)})`, + `'${server.hostname}' security level weakened to ${server.hackDifficulty}. Gained ${formatExp( + expGain, + )} hacking exp (t=${formatThreads(threads)})`, ); - return helpers.netscriptDelay(ctx, weakenTime * 1000).then(function () { - const host = GetServer(ctx.workerScript.hostname); - if (host === null) { - helpers.log(ctx, () => "Server is null, did it die?"); - return Promise.resolve(0); - } - const cores = helpers.getServer(ctx, ctx.workerScript.hostname).cpuCores; - const coreBonus = getCoreBonus(cores); - const weakenAmt = CONSTANTS.ServerWeakenAmount * threads * coreBonus; - server.weaken(weakenAmt); - ctx.workerScript.scriptRef.recordWeaken(server.hostname, threads); - const expGain = calculateHackingExpGain(server, Player) * threads; - helpers.log( - ctx, - () => - `'${server.hostname}' security level weakened to ${server.hackDifficulty}. Gained ${formatExp( - expGain, - )} hacking exp (t=${formatThreads(threads)})`, - ); - ctx.workerScript.scriptRef.onlineExpGained += expGain; - Player.gainHackingExp(expGain); - // Account for hidden multiplier in Server.weaken() - return Promise.resolve(weakenAmt * currentNodeMults.ServerWeakenRate); - }); - }, + ctx.workerScript.scriptRef.onlineExpGained += expGain; + Player.gainHackingExp(expGain); + // Account for hidden multiplier in Server.weaken() + return Promise.resolve(weakenAmt * currentNodeMults.ServerWeakenRate); + }); + }, weakenAnalyze: (ctx) => (_threads, _cores = 1) => { diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index a6266fa5d..b54f605ba 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -530,7 +530,7 @@ export function NetscriptSingularity(): InternalAPI { manualHack: (ctx) => () => { helpers.checkSingularityAccess(ctx); const server = Player.getCurrentServer(); - return helpers.hack(ctx, server.hostname, true); + return helpers.hack(ctx, server.hostname, true, null); }, installBackdoor: (ctx) => async (): Promise => { helpers.checkSingularityAccess(ctx); diff --git a/src/NetscriptFunctions/UserInterface.ts b/src/NetscriptFunctions/UserInterface.ts index 8a0b1bc35..245118726 100644 --- a/src/NetscriptFunctions/UserInterface.ts +++ b/src/NetscriptFunctions/UserInterface.ts @@ -5,9 +5,43 @@ import { defaultTheme } from "../Themes/Themes"; import { defaultStyles } from "../Themes/Styles"; import { CONSTANTS } from "../Constants"; import { hash } from "../hash/hash"; -import { InternalAPI } from "../Netscript/APIWrapper"; +import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper"; import { Terminal } from "../../src/Terminal"; -import { helpers, assertObjectType } from "../Netscript/NetscriptHelpers"; +import { helpers, makeRuntimeErrorMsg } from "../Netscript/NetscriptHelpers"; + +/** Will probably remove the below function in favor of a different approach to object type assertion. + * This method cannot be used to handle optional properties. */ +export function assertObjectType( + ctx: NetscriptContext, + name: string, + obj: unknown, + desiredObject: T, +): asserts obj is T { + if (typeof obj !== "object" || obj === null) { + throw makeRuntimeErrorMsg( + ctx, + `Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`, + "TYPE", + ); + } + for (const [key, val] of Object.entries(desiredObject)) { + if (!Object.hasOwn(obj, key)) { + throw makeRuntimeErrorMsg( + ctx, + `Object provided for argument ${name} is missing required property ${key}.`, + "TYPE", + ); + } + const objVal = (obj as Record)[key]; + if (typeof val !== typeof objVal) { + throw makeRuntimeErrorMsg( + ctx, + `Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`, + "TYPE", + ); + } + } +} export function NetscriptUserInterface(): InternalAPI { return {