Skip to content

Commit 78157a1

Browse files
committed
fix(frontend): guard against invalid dropped file paths
1 parent 178ba2f commit 78157a1

7 files changed

Lines changed: 61 additions & 41 deletions

File tree

frontend/src/hooks/useContentPage.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { GetContentRoots } from "bindings/github.com/liteldev/LeviLauncher/conte
1212
import * as types from "bindings/github.com/liteldev/LeviLauncher/internal/types/models";
1313
import { readCurrentVersionName } from "@/utils/currentVersion";
1414
import { compareVersions } from "@/utils/version";
15-
import { countDirectories } from "@/utils/fs";
15+
import { countDirectories, getPathBaseName, normalizeDroppedFiles } from "@/utils/fs";
1616
import { getPlayerGamertagMap, listPlayers } from "@/utils/content";
1717
import * as minecraft from "bindings/github.com/liteldev/LeviLauncher/minecraft";
1818
import * as contentService from "bindings/github.com/liteldev/LeviLauncher/contentservice";
@@ -308,22 +308,23 @@ export const useContentPage = (t: TFunc) => {
308308

309309
const doImportFromPaths = async (paths: string[]) => {
310310
try {
311-
if (!paths?.length) return;
311+
const normalizedPaths = normalizeDroppedFiles(paths);
312+
if (!normalizedPaths.length) return;
312313
const name = currentVersionName || readCurrentVersionName();
313314
if (!name) {
314315
setErrorMsg(t("launcherpage.currentVersion_none") as string);
315316
return;
316317
}
317-
const hasWorld = paths.some((p) => p?.toLowerCase().endsWith(".mcworld"));
318+
const hasWorld = normalizedPaths.some((p) =>
319+
p.toLowerCase().endsWith(".mcworld"),
320+
);
318321
let hasSkin = false;
319-
if (paths.length > 0) {
322+
if (normalizedPaths.length > 0) {
320323
setImporting(true);
321-
const firstBase =
322-
paths[0].replace(/\\/g, "/").split("/").pop() || paths[0];
323-
setCurrentFile(firstBase);
324+
setCurrentFile(getPathBaseName(normalizedPaths[0]));
324325
}
325-
for (const p of paths) {
326-
if (p?.toLowerCase().endsWith(".mcpack")) {
326+
for (const p of normalizedPaths) {
327+
if (p.toLowerCase().endsWith(".mcpack")) {
327328
const isSkin = await (contentService as any)?.IsMcpackSkinPackPath?.(
328329
p,
329330
);
@@ -338,7 +339,7 @@ export const useContentPage = (t: TFunc) => {
338339
const needsPlayer = hasWorld || (hasSkin && !isSharedMode);
339340

340341
if (needsPlayer) {
341-
pendingImportPathsRef.current = paths;
342+
pendingImportPathsRef.current = normalizedPaths;
342343
playerSelectOnOpen();
343344
chosenPlayer = await new Promise<string>((resolve) => {
344345
playerSelectResolveRef.current = resolve;
@@ -356,7 +357,7 @@ export const useContentPage = (t: TFunc) => {
356357
const pathsToImport =
357358
pendingImportPathsRef.current.length > 0
358359
? pendingImportPathsRef.current
359-
: paths;
360+
: normalizedPaths;
360361
pendingImportPathsRef.current = [];
361362
const playerToUse = chosenPlayer || selectedPlayer || "";
362363
for (const p of pathsToImport) {
@@ -366,7 +367,7 @@ export const useContentPage = (t: TFunc) => {
366367
setImporting(true);
367368
started = true;
368369
}
369-
const base = p.replace(/\\/g, "/").split("/").pop() || p;
370+
const base = getPathBaseName(p);
370371
setCurrentFile(base);
371372

372373
let err = "";
@@ -442,7 +443,7 @@ export const useContentPage = (t: TFunc) => {
442443
setImporting(true);
443444
started = true;
444445
}
445-
const base = p.replace(/\\/g, "/").split("/").pop() || p;
446+
const base = getPathBaseName(p);
446447
setCurrentFile(base);
447448
let err = "";
448449
if (
@@ -503,7 +504,7 @@ export const useContentPage = (t: TFunc) => {
503504
}
504505
succFiles.push(base);
505506
} else if (lower.endsWith(".mcworld")) {
506-
const base = p.replace(/\\/g, "/").split("/").pop() || p;
507+
const base = getPathBaseName(p);
507508
if (!playerToUse) {
508509
errPairs.push({ name: base, err: "ERR_NO_PLAYER" });
509510
continue;
@@ -683,8 +684,7 @@ export const useContentPage = (t: TFunc) => {
683684
for (const pack of packs) {
684685
const packPath = String(pack?.path || "").trim();
685686
if (!packPath) continue;
686-
const fallbackName =
687-
packPath.replace(/\\/g, "/").split("/").pop() || packPath;
687+
const fallbackName = getPathBaseName(packPath);
688688
const packName = String(pack?.manifest?.name || fallbackName);
689689
const itemLabel = `${packName} -> ${targetName}`;
690690
setCurrentFile(itemLabel);
@@ -774,8 +774,9 @@ export const useContentPage = (t: TFunc) => {
774774
React.useEffect(() => {
775775
return Events.On("files-dropped", (event) => {
776776
const data = (event.data as { files: string[] }) || {};
777-
if (data.files && data.files.length > 0) {
778-
void doImportRef.current(data.files);
777+
const files = normalizeDroppedFiles(data.files);
778+
if (files.length > 0) {
779+
void doImportRef.current(files);
779780
}
780781
});
781782
}, []);

frontend/src/hooks/useModsPage.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { useModIntelligence } from "@/utils/ModIntelligenceContext";
3131
import { useLipTaskConsole } from "@/utils/LipTaskConsoleContext";
3232
import { setNavLockReason } from "@/hooks/useAppNavigation";
3333
import { ROUTES } from "@/constants/routes";
34+
import { getPathBaseName, normalizeDroppedFiles } from "@/utils/fs";
3435

3536
const LEVILAMINA_NORMALIZED = "levilamina";
3637
const ERR_LL_MANAGED_IN_VERSION_SETTINGS = "ERR_LL_MANAGED_IN_VERSION_SETTINGS";
@@ -982,7 +983,8 @@ export const useModsPage = (
982983

983984
const doImportFromPaths = async (paths: string[]) => {
984985
try {
985-
if (!paths?.length) return;
986+
const normalizedPaths = normalizeDroppedFiles(paths);
987+
if (!normalizedPaths.length) return;
986988
const name = activeVersionName;
987989
if (!name) {
988990
setErrorMsg(t("launcherpage.currentVersion_none") as string);
@@ -991,14 +993,14 @@ export const useModsPage = (
991993
let started = false;
992994
const succFiles: string[] = [];
993995
const errPairs: Array<{ name: string; err: string }> = [];
994-
for (const p of paths) {
996+
for (const p of normalizedPaths) {
995997
const lower = p.toLowerCase();
996998
if (lower.endsWith(".zip")) {
997999
if (!started) {
9981000
setImporting(true);
9991001
started = true;
10001002
}
1001-
const base = p.replace(/\\/g, "/").split("/").pop() || p;
1003+
const base = getPathBaseName(p);
10021004
setCurrentFile(base);
10031005
let err = await ImportModZipPath(name, p, false);
10041006
if (err) {
@@ -1024,7 +1026,7 @@ export const useModsPage = (
10241026
}
10251027
succFiles.push(base);
10261028
} else if (lower.endsWith(".dll")) {
1027-
const base = p.replace(/\\/g, "/").split("/").pop() || "";
1029+
const base = getPathBaseName(p);
10281030
const baseNoExt = base.replace(/\.[^/.]+$/, "");
10291031
setDllName(baseNoExt);
10301032
setDllType("preload-native");
@@ -1113,8 +1115,9 @@ export const useModsPage = (
11131115
useEffect(() => {
11141116
return Events.On("files-dropped", (event) => {
11151117
const data = (event.data as { files: string[] }) || {};
1116-
if (data.files && data.files.length > 0) {
1117-
void doImportRef.current(data.files);
1118+
const files = normalizeDroppedFiles(data.files);
1119+
if (files.length > 0) {
1120+
void doImportRef.current(files);
11181121
}
11191122
});
11201123
}, []);

frontend/src/pages/BehaviorPacksPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ import { useContentSort } from "@/hooks/useContentSort";
6868
import { formatBytes } from "@/utils/formatting";
6969
import { compareVersions } from "@/utils/version";
7070
import { ImportResultModal } from "@/components/ImportResultModal";
71+
import { getPathBaseName } from "@/utils/fs";
7172

72-
const getNameFn = (p: any) => String(p.name || p.path?.split("\\").pop() || "");
73+
const getNameFn = (p: any) => String(p.name || getPathBaseName(p.path) || "");
7374
const getTimeFn = (p: any) => Number(p.modTime || 0);
7475
type TransferTargetVersion = {
7576
name: string;
@@ -437,7 +438,7 @@ export default function BehaviorPacksPage() {
437438
const packNameMap = new Map<string, string>(
438439
packs.map((pack: any) => {
439440
const path = String(pack?.path || "");
440-
const fallbackName = path.replace(/\\/g, "/").split("/").pop() || path;
441+
const fallbackName = getPathBaseName(path);
441442
const displayName = String(pack?.name || fallbackName);
442443
return [path, displayName];
443444
}),
@@ -452,8 +453,7 @@ export default function BehaviorPacksPage() {
452453

453454
for (const targetName of targetNames) {
454455
for (const packPath of selectedPaths) {
455-
const fallbackName =
456-
packPath.replace(/\\/g, "/").split("/").pop() || packPath;
456+
const fallbackName = getPathBaseName(packPath);
457457
const packName = packNameMap.get(packPath) || fallbackName;
458458
const itemLabel = `${packName} -> ${targetName}`;
459459
setCurrentTransferItem(itemLabel);
@@ -754,7 +754,7 @@ export default function BehaviorPacksPage() {
754754
className="text-lg font-bold text-default-900 dark:text-white truncate"
755755
title={p.name}
756756
>
757-
{renderMcText(p.name || p.path.split("\\").pop())}
757+
{renderMcText(p.name || getPathBaseName(p.path))}
758758
</h3>
759759
</div>
760760

frontend/src/pages/ResourcePacksPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ import { useContentSort } from "@/hooks/useContentSort";
7979
import { formatBytes, formatDate } from "@/utils/formatting";
8080
import { compareVersions } from "@/utils/version";
8181
import { ImportResultModal } from "@/components/ImportResultModal";
82+
import { getPathBaseName } from "@/utils/fs";
8283

83-
const getNameFn = (p: any) => String(p.name || p.path?.split("\\").pop() || "");
84+
const getNameFn = (p: any) => String(p.name || getPathBaseName(p.path) || "");
8485
const getTimeFn = (p: any) => Number(p.modTime || 0);
8586
type TransferTargetVersion = {
8687
name: string;
@@ -509,7 +510,7 @@ export default function ResourcePacksPage() {
509510
const packNameMap = new Map<string, string>(
510511
packs.map((pack: any) => {
511512
const path = String(pack?.path || "");
512-
const fallbackName = path.replace(/\\/g, "/").split("/").pop() || path;
513+
const fallbackName = getPathBaseName(path);
513514
const displayName = String(pack?.name || fallbackName);
514515
return [path, displayName];
515516
}),
@@ -524,8 +525,7 @@ export default function ResourcePacksPage() {
524525

525526
for (const targetName of targetNames) {
526527
for (const packPath of selectedPaths) {
527-
const fallbackName =
528-
packPath.replace(/\\/g, "/").split("/").pop() || packPath;
528+
const fallbackName = getPathBaseName(packPath);
529529
const packName = packNameMap.get(packPath) || fallbackName;
530530
const itemLabel = `${packName} -> ${targetName}`;
531531
setCurrentTransferItem(itemLabel);
@@ -838,7 +838,7 @@ export default function ResourcePacksPage() {
838838
className="text-lg font-bold text-default-900 dark:text-white truncate"
839839
title={p.name}
840840
>
841-
{renderMcText(p.name || p.path.split("\\").pop())}
841+
{renderMcText(p.name || getPathBaseName(p.path))}
842842
</h3>
843843
</div>
844844

frontend/src/pages/SkinPacksPage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ import { useSelectionMode } from "@/hooks/useSelectionMode";
7777
import { useContentSort } from "@/hooks/useContentSort";
7878
import { formatBytes, formatDate } from "@/utils/formatting";
7979
import { ImportResultModal } from "@/components/ImportResultModal";
80+
import { getPathBaseName } from "@/utils/fs";
8081

81-
const getNameFn = (p: any) => String(p.name || p.path?.split("\\").pop() || "");
82+
const getNameFn = (p: any) => String(p.name || getPathBaseName(p.path) || "");
8283
const getTimeFn = (p: any) => Number(p.modTime || 0);
8384
type TransferTargetVersion = {
8485
name: string;
@@ -468,7 +469,7 @@ export default function SkinPacksPage() {
468469
const packNameMap = new Map<string, string>(
469470
packs.map((pack: any) => {
470471
const path = String(pack?.path || "");
471-
const fallbackName = path.replace(/\\/g, "/").split("/").pop() || path;
472+
const fallbackName = getPathBaseName(path);
472473
const displayName = String(pack?.name || fallbackName);
473474
return [path, displayName];
474475
}),
@@ -483,8 +484,7 @@ export default function SkinPacksPage() {
483484

484485
for (const targetName of targetNames) {
485486
for (const packPath of selectedPaths) {
486-
const fallbackName =
487-
packPath.replace(/\\/g, "/").split("/").pop() || packPath;
487+
const fallbackName = getPathBaseName(packPath);
488488
const packName = packNameMap.get(packPath) || fallbackName;
489489
const itemLabel = `${packName} -> ${targetName}`;
490490
setCurrentTransferItem(itemLabel);
@@ -857,7 +857,7 @@ export default function SkinPacksPage() {
857857
className="text-lg font-bold text-default-900 dark:text-white truncate"
858858
title={p.name}
859859
>
860-
{renderMcText(p.name || p.path.split("\\").pop())}
860+
{renderMcText(p.name || getPathBaseName(p.path))}
861861
</h3>
862862
</div>
863863

frontend/src/pages/WorldsListPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import { useSelectionMode } from "@/hooks/useSelectionMode";
8080
import { useContentSort } from "@/hooks/useContentSort";
8181
import { formatBytes } from "@/utils/formatting";
8282
import { compareVersions } from "@/utils/version";
83+
import { getPathBaseName } from "@/utils/fs";
8384

8485
interface WorldInfo {
8586
Path: string;
@@ -457,7 +458,7 @@ export default function WorldsListPage() {
457458
const worldNameMap = new Map<string, string>(
458459
worlds.map((w) => [
459460
w.Path,
460-
w.FolderName || w.Path.split("\\").pop() || w.Path,
461+
w.FolderName || getPathBaseName(w.Path) || w.Path,
461462
]),
462463
);
463464

@@ -472,7 +473,7 @@ export default function WorldsListPage() {
472473
for (const worldPath of selectedWorldPaths) {
473474
const worldName =
474475
worldNameMap.get(worldPath) ||
475-
worldPath.split("\\").pop() ||
476+
getPathBaseName(worldPath) ||
476477
worldPath;
477478
const itemLabel = `${worldName} -> ${targetName}`;
478479
setCurrentTransferItem(itemLabel);

frontend/src/utils/fs.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import { ListDir } from "bindings/github.com/liteldev/LeviLauncher/minecraft";
22

33
export type DirEntry = { name: string; path: string };
44

5+
export function getPathBaseName(path: unknown): string {
6+
const value = String(path || "").trim();
7+
if (!value) return "";
8+
const parts = value.replace(/\\/g, "/").split("/").filter(Boolean);
9+
return parts.at(-1) || value;
10+
}
11+
12+
export function normalizeDroppedFiles(files: unknown): string[] {
13+
if (!Array.isArray(files)) return [];
14+
15+
return files
16+
.map((item) => (typeof item === "string" ? item.trim() : ""))
17+
.filter(Boolean);
18+
}
19+
520
export async function listDirectories(path: string): Promise<DirEntry[]> {
621
try {
722
const list = await ListDir(path);

0 commit comments

Comments
 (0)