From c8b8bcabc80f94838b4af23cbac320f8f2069420 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Wed, 21 Aug 2024 22:26:56 -0400 Subject: [PATCH] Replace illegal fs chars in filenames --- backend/endpoints/rom.py | 31 ++++++++++------- backend/utils/filesystem.py | 33 +++++++++++++++++++ .../components/common/Game/Dialog/EditRom.vue | 22 ++++--------- 3 files changed, 58 insertions(+), 28 deletions(-) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index c7d1f1301..a964861c4 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -25,6 +25,7 @@ from logger.logger import log from starlette.requests import ClientDisconnect from streaming_form_data import StreamingFormDataParser from streaming_form_data.targets import FileTarget, NullTarget +from utils.filesystem import sanitize_filename from utils.hashing import crc32_to_hex from utils.nginx import ZipContentLine, ZipResponse from utils.router import APIRouter @@ -337,19 +338,25 @@ async def update_rom( } ) - fs_safe_file_name = data.get("file_name", rom.file_name).strip().replace("/", "-") - fs_safe_name = cleaned_data["name"].strip().replace("/", "-") - - if rename_as_source: - fs_safe_file_name = rom.file_name.replace( - rom.file_name_no_tags or rom.file_name_no_ext, fs_safe_name - ) + new_file_name = data.get("file_name", rom.file_name) try: - if rom.file_name != fs_safe_file_name: + if rename_as_source: + new_file_name = rom.file_name.replace( + rom.file_name_no_tags or rom.file_name_no_ext, + data.get("name", rom.name), + ) + new_file_name = sanitize_filename(new_file_name) fs_rom_handler.rename_file( old_name=rom.file_name, - new_name=fs_safe_file_name, + new_name=new_file_name, + file_path=rom.file_path, + ) + elif rom.file_name != new_file_name: + new_file_name = sanitize_filename(new_file_name) + fs_rom_handler.rename_file( + old_name=rom.file_name, + new_name=new_file_name, file_path=rom.file_path, ) except RomAlreadyExistsException as exc: @@ -360,12 +367,12 @@ async def update_rom( cleaned_data.update( { - "file_name": fs_safe_file_name, + "file_name": new_file_name, "file_name_no_tags": fs_rom_handler.get_file_name_with_no_tags( - fs_safe_file_name + new_file_name ), "file_name_no_ext": fs_rom_handler.get_file_name_with_no_extension( - fs_safe_file_name + new_file_name ), } ) diff --git a/backend/utils/filesystem.py b/backend/utils/filesystem.py index 3db3fabde..018368790 100644 --- a/backend/utils/filesystem.py +++ b/backend/utils/filesystem.py @@ -1,4 +1,5 @@ import os +import re from collections.abc import Iterator from pathlib import Path @@ -27,3 +28,35 @@ def iter_directories(path: str, recursive: bool = False) -> Iterator[tuple[Path, yield Path(root), directory if not recursive: break + + +INVALID_CHARS = r'[\\/:*?"<>|]' + + +def sanitize_filename(filename): + """ + Replace invalid characters in the filename to make it valid across common filesystems + + Args: + - filename (str): The filename to sanitize. + + Returns: + - str: The sanitized filename. + """ + # Replace some invalid characters with hyphen + sanitized_filename = re.sub(r"[\\/:|]", "-", filename) + + # Remove other invalid characters + sanitized_filename = re.sub(r'[*?"<>]', "", sanitized_filename) + + # Ensure null bytes are not included (ZFS allows any characters except null bytes) + sanitized_filename = sanitized_filename.replace("\0", "") + + # Remove leading/trailing whitespace + sanitized_filename = sanitized_filename.strip() + + # Ensure the filename is not empty + if not sanitized_filename: + raise ValueError("Filename cannot be empty after sanitization") + + return sanitized_filename diff --git a/frontend/src/components/common/Game/Dialog/EditRom.vue b/frontend/src/components/common/Game/Dialog/EditRom.vue index e23239f56..d81bcaac7 100644 --- a/frontend/src/components/common/Game/Dialog/EditRom.vue +++ b/frontend/src/components/common/Game/Dialog/EditRom.vue @@ -20,10 +20,6 @@ const rom = ref(); const romsStore = storeRoms(); const imagePreviewUrl = ref(""); const removeCover = ref(false); -const fileNameInputRules = { - required: (value: string) => !!value || "Required", - newFileName: (value: string) => !value.includes("/") || "Invalid characters", -}; const emitter = inject>("emitter"); emitter?.on("showEditRomDialog", (romToEdit: UpdateRom | undefined) => { show.value = true; @@ -62,16 +58,9 @@ async function removeArtwork() { async function updateRom() { if (!rom.value) return; - if (rom.value.file_name.includes("/")) { + if (!rom.value.file_name) { emitter?.emit("snackbarShow", { - msg: "Couldn't edit rom: invalid file name characters", - icon: "mdi-close-circle", - color: "red", - }); - return; - } else if (!rom.value.file_name) { - emitter?.emit("snackbarShow", { - msg: "Couldn't edit rom: file name required", + msg: "Cannot save: file name is required", icon: "mdi-close-circle", color: "red", }); @@ -145,8 +134,7 @@ function closeDialog() { v-model="rom.file_name" class="py-2" :rules="[ - fileNameInputRules.newFileName, - fileNameInputRules.required, + (value: string) => !!value ]" label="File name" variant="outlined" @@ -177,7 +165,9 @@ function closeDialog() {