diff --git a/server/src/api/ir/fervidex/fervidex.test.ts b/server/src/api/ir/fervidex/fervidex.test.ts index 5de7d7bb4..8ff60c74b 100644 --- a/server/src/api/ir/fervidex/fervidex.test.ts +++ b/server/src/api/ir/fervidex/fervidex.test.ts @@ -101,6 +101,26 @@ t.test("POST /api/ir/fervidex/score/submit", async (t) => { t.end(); }); + t.test("Should import a valid score with 2dx-gsm", async (t) => { + let res = await mockApi + .post("/api/ir/fervidex/score/submit") + .set("Cookie", cookie) + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(GetKTDataJSON("./fervidex/2dxgsm.json")); + + t.equal(res.body.success, true, "Should be successful"); + + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + + let scores = await db.scores.count({ + service: "Fervidex", + }); + + t.equal(scores, 1, "Should import 1 score."); + + t.end(); + }); + t.test("Should reject an invalid body", async (t) => { let res = await mockApi .post("/api/ir/fervidex/score/submit") diff --git a/server/src/api/ir/fervidex/fervidex.ts b/server/src/api/ir/fervidex/fervidex.ts index 4e471d421..b918f2e38 100644 --- a/server/src/api/ir/fervidex/fervidex.ts +++ b/server/src/api/ir/fervidex/fervidex.ts @@ -1,5 +1,7 @@ import { Router, RequestHandler } from "express"; import { GetUserWithIDGuaranteed } from "../../../common/user"; +import { ParseEA3SoftID } from "../../../common/util"; +import { IIDX_AC_MODEL, INF2_MODEL, REV_2DXBMS } from "../../../constants/ea3id"; import { RequireLoggedIn } from "../../../middleware/require-logged-in"; import { ExpressWrappedScoreImportMain } from "../../../score-import/framework/express-wrapper"; import { ParseFervidexStatic } from "../../../score-import/import-types/ir/fervidex-static/parser"; @@ -10,10 +12,26 @@ const router: Router = Router({ mergeParams: true }); const RequireInf2ModelHeader: RequestHandler = async (req, res, next) => { let swModel = req.header("X-Software-Model"); - if (!swModel || !swModel.startsWith("P2D:J:B:A")) { - return res.status(400).send({ + if (!swModel) { + return res.status(400).json({ success: false, - description: "This endpoint is only available for INF2 clients.", + description: `Invalid X-Software-Model.`, + }); + } + + try { + let softID = ParseEA3SoftID(swModel); + + if (softID.model !== INF2_MODEL) { + return res.status(400).send({ + success: false, + description: "This endpoint is only available for INF2 clients.", + }); + } + } catch (err) { + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, }); } @@ -23,8 +41,27 @@ const RequireInf2ModelHeader: RequestHandler = async (req, res, next) => { const ValidateModelHeader: RequestHandler = async (req, res, next) => { let swModel = req.header("X-Software-Model"); - if (!swModel || swModel.startsWith("LDJ:J:B:X") || swModel.startsWith("LDJ:J:B:Z")) { - return res.status(400).send({ success: false, description: "Invalid X-Software-Model." }); + if (!swModel) { + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); + } + + try { + let softID = ParseEA3SoftID(swModel); + + if (softID.rev !== REV_2DXBMS) { + return res.status(400).send({ + success: false, + description: "2DX_BMS is not supported.", + }); + } + } catch (err) { + return res.status(400).json({ + success: false, + description: `Invalid X-Software-Model.`, + }); } return next(); diff --git a/server/src/common/database-lookup/chart.ts b/server/src/common/database-lookup/chart.ts index 30a8c61af..802ec32b6 100644 --- a/server/src/common/database-lookup/chart.ts +++ b/server/src/common/database-lookup/chart.ts @@ -1,4 +1,3 @@ -import { version } from "commander"; import { Difficulties, Game, integer, Playtypes, IDStrings } from "kamaitachi-common"; import db from "../../db/db"; @@ -96,6 +95,46 @@ export function FindChartOnInGameID( }); } +/** + * Finds a non-custom chart on its in-game-ID, playtype and difficulty. + * This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate. + */ +export function FindIIDXChartOnInGameID( + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings] +) { + // @todo throw an error if this is called with a game that doesn't + // support InGameID. + return db.charts.iidx.findOne({ + "data.inGameID": inGameID, + "flags.2dxtra": false, + playtype, + difficulty, + }); +} + +/** + * Finds a non-custom chart on its in-game-ID, playtype and difficulty. + * This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate. + */ +export function FindIIDXChartOnInGameIDVersion( + inGameID: number, + playtype: Playtypes[Game], + difficulty: Difficulties[IDStrings], + version: string +) { + // @todo throw an error if this is called with a game that doesn't + // support InGameID. + return db.charts.iidx.findOne({ + "data.inGameID": inGameID, + "flags.2dxtra": false, + playtype, + difficulty, + versions: version, + }); +} + /** * Find a chart on its in-game-ID, playtype, difficulty and version. */ @@ -114,6 +153,15 @@ export function FindChartOnInGameIDVersion( }); } +/** + * Finds an IIDX chart on its 2dxtra hash, which is the sha256 of the .1 buffer. + */ +export function FindIIDXChartWith2DXtraHash(hash: string) { + return db.charts.iidx.findOne({ + "data.hashSHA256": hash, + }); +} + /** * Find an SDVX Chart on its in game ID. This exists to handle * oddities with SDVX difficulties - If "ANY_INF" is sent, it actually diff --git a/server/src/common/escape-string-regexp.ts b/server/src/common/util.ts similarity index 56% rename from server/src/common/escape-string-regexp.ts rename to server/src/common/util.ts index f4b6f72f0..c143d59c3 100644 --- a/server/src/common/escape-string-regexp.ts +++ b/server/src/common/util.ts @@ -3,7 +3,7 @@ // which really really messes with a lot of the ecosystem. // shim. -export default function EscapeStringRegexp(string: string) { +export function EscapeStringRegexp(string: string) { if (typeof string !== "string") { throw new TypeError("Expected a string"); } @@ -12,3 +12,23 @@ export default function EscapeStringRegexp(string: string) { // Use a simple backslash escape when it's always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns' stricter grammar. return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d"); } + +export function ParseEA3SoftID(ver: string) { + let a = ver.split(":"); + + if (a.length !== 5) { + throw new Error(`Invalid Version Code. Had ${a.length} components.`); + } + + if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) { + throw new Error(`Invalid Version Code.`); + } + + return { + model: a[0], + dest: a[1], // region + spec: a[2], + rev: a[3], + ext: a[4], + }; +} diff --git a/server/src/constants/ea3id.ts b/server/src/constants/ea3id.ts new file mode 100644 index 000000000..e35024b1d --- /dev/null +++ b/server/src/constants/ea3id.ts @@ -0,0 +1,7 @@ +export const INF2_MODEL = "P2D"; +export const IIDX_AC_MODEL = "LDJ"; +export const IIDX_AC_LIGHTNING_MODEL = "TDJ"; + +export const REV_2DXTRA = "E"; +export const REV_2DXBMS = "Z"; +export const REV_OMNIMIX = "X"; diff --git a/server/src/logger.test.ts b/server/src/logger.test.ts index 7bd14c600..23c57b7bf 100644 --- a/server/src/logger.test.ts +++ b/server/src/logger.test.ts @@ -1,4 +1,5 @@ import t from "tap"; +import { CloseMongoConnection } from "./db/db"; import CreateLogCtx, { Transports } from "./logger"; t.test("Logger Tests", (t) => { @@ -20,3 +21,5 @@ t.test("Logger Tests", (t) => { t.end(); }); + +t.teardown(CloseMongoConnection); diff --git a/server/src/score-import/framework/common/import-logger.test.ts b/server/src/score-import/framework/common/import-logger.test.ts index b980aba7d..16048d404 100644 --- a/server/src/score-import/framework/common/import-logger.test.ts +++ b/server/src/score-import/framework/common/import-logger.test.ts @@ -1,6 +1,7 @@ import t from "tap"; import { CreateImportLoggerAndID } from "./import-logger"; import { PublicUserDocument } from "kamaitachi-common"; +import { CloseMongoConnection } from "../../../db/db"; t.test("#CreateImportLoggerAndID", (t) => { let { importID, logger } = CreateImportLoggerAndID( @@ -17,3 +18,5 @@ t.test("#CreateImportLoggerAndID", (t) => { t.end(); }); + +t.teardown(CloseMongoConnection); diff --git a/server/src/score-import/import-types/common/batch-manual/converter.test.ts b/server/src/score-import/import-types/common/batch-manual/converter.test.ts index 144d18030..e947ee413 100644 --- a/server/src/score-import/import-types/common/batch-manual/converter.test.ts +++ b/server/src/score-import/import-types/common/batch-manual/converter.test.ts @@ -4,7 +4,7 @@ import CreateLogCtx from "../../../../logger"; import { Testing511Song, Testing511SPA } from "../../../../test-utils/test-data"; import { ResolveChartFromSong, ResolveMatchTypeToKTData, ConverterBatchManual } from "./converter"; import deepmerge from "deepmerge"; -import EscapeStringRegexp from "../../../../common/escape-string-regexp"; +import EscapeStringRegexp from "../../../../common/util"; import { CloseMongoConnection } from "../../../../db/db"; import { Game } from "kamaitachi-common"; import ResetDBState from "../../../../test-utils/reset-db-state"; diff --git a/server/src/score-import/import-types/ir/fervidex/converter.ts b/server/src/score-import/import-types/ir/fervidex/converter.ts index f64d02296..88011f5d6 100644 --- a/server/src/score-import/import-types/ir/fervidex/converter.ts +++ b/server/src/score-import/import-types/ir/fervidex/converter.ts @@ -11,7 +11,10 @@ import { } from "../../../framework/score-importing/converter-failures"; import { FervidexContext, FervidexScore } from "./types"; import { Lamps, Grades, Difficulties, Playtypes } from "kamaitachi-common"; -import { FindChartOnInGameIDVersion } from "../../../../common/database-lookup/chart"; +import { + FindIIDXChartOnInGameIDVersion, + FindIIDXChartWith2DXtraHash, +} from "../../../../common/database-lookup/chart"; export const FERVIDEX_LAMP_LOOKUP = { 0: "NO PLAY", @@ -139,13 +142,17 @@ export const ConverterIRFervidex: ConverterFunction { let { difficulty, playtype } = SplitFervidexChartRef(data.chart); - let chart = await FindChartOnInGameIDVersion( - "iidx", - data.entry_id, - playtype, - difficulty, - context.version - ); + let chart; + if (data.custom) { + chart = await FindIIDXChartWith2DXtraHash(data.chart_sha256); + } else { + chart = await FindIIDXChartOnInGameIDVersion( + data.entry_id, + playtype, + difficulty, + context.version + ); + } if (!chart) { throw new KTDataNotFoundFailure( @@ -204,7 +211,7 @@ export const ConverterIRFervidex: ConverterFunction