From 23721246bdf044984d796840c4ed60ebadfcb1e7 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Fri, 24 Oct 2025 17:25:32 +0100 Subject: [PATCH] refactor: improve path sanitization and remove unused chat area code - Remove unused conversationKey and incomplete message sorting logic - Add stack limit protection to conversation naming loop - Enhance UTF-8 truncation to preserve character boundaries - Add empty segment fallback and improve documentation - Fix VaultService.exists to respect access restrictions --- Components/ChatArea.svelte | 7 ++--- Services/ConversationNamingService.ts | 6 ++++ Services/SanitiserService.ts | 42 ++++++++++++++++++++++----- Services/VaultService.ts | 2 +- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 5aa608f..7f88e99 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -18,13 +18,10 @@ export let currentStreamingMessageId: string | null = null; export let editModeActive: boolean = false; - let conversationKey = 0; - export function resetChatArea() { messageElements = []; lastProcessedContent.clear(); currentStreamFinalized = false; - conversationKey++; // Force complete re-render if (chatAreaPaddingElement) { chatAreaPaddingElement.style.padding = "0px"; } @@ -35,13 +32,13 @@ tick().then(() => { settled = false; - const lastMessage = messageElements.sort((a, b) => a.index - b.index).last(); - if (!lastMessage || !chatAreaPaddingElement) { + if (messageElements.length === 0 || !chatAreaPaddingElement) { if (chatAreaPaddingElement) { chatAreaPaddingElement.style.padding = "0px"; } return; } + messageElements.sort((a, b) => a.index - b.index)[messageElements.length - 1]; const gap = parseFloat(getComputedStyle(chatContainer).gap) || 0; const paddingTop = parseFloat(getComputedStyle(chatContainer).paddingTop) || 0; diff --git a/Services/ConversationNamingService.ts b/Services/ConversationNamingService.ts index f043092..1db5aa2 100644 --- a/Services/ConversationNamingService.ts +++ b/Services/ConversationNamingService.ts @@ -7,6 +7,8 @@ import type { VaultService } from "./VaultService"; import { Path } from "Enums/Path"; export class ConversationNamingService { + private readonly stackLimit: number = 1000; + private namingProvider: IConversationNamingService | undefined; private conversationService: ConversationFileSystemService; private vaultService: VaultService; @@ -60,6 +62,10 @@ export class ConversationNamingService { while (this.vaultService.exists(`${Path.Conversations}/${availableTitle}.json`, true)) { availableTitle = `${cleanedTitle}(${index})`; index++; + + if (index > this.stackLimit) { + throw new Error(`Stack limit reached when trying to generate conversation name for "${cleanedTitle}"`); + } } return availableTitle; } diff --git a/Services/SanitiserService.ts b/Services/SanitiserService.ts index 27d48af..0594951 100644 --- a/Services/SanitiserService.ts +++ b/Services/SanitiserService.ts @@ -11,7 +11,7 @@ export class SanitiserService { private readonly windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; private readonly windowsTrailingRe = /[\. ]+$/; - constructor() {} + constructor() { } /** * Sanitizes a file path with directories, removing illegal characters and ensuring cross-platform compatibility @@ -37,7 +37,8 @@ export class SanitiserService { const driveLetter = driveMatch ? driveMatch[1] : ''; // Split by both forward and back slashes - // This handles mixed separators like: /home/user\docs/file.txt + // Note: Forward slashes and backslashes are treated as path separators, + // not as illegal characters within segments. This is intentional for cross-platform compatibility. let segments = input.split(/[\\\/]+/); // Remove empty segments (from leading/trailing slashes or multiple consecutive slashes) @@ -92,6 +93,9 @@ export class SanitiserService { /** * Sanitizes a single path segment (filename or directory name) + * @param segment - The path segment to sanitize + * @param replacement - Character to replace illegal characters with + * @returns Sanitized segment, or a fallback value if the result would be empty */ private sanitizeSegment(segment: string, replacement: string): string { if (!segment || segment === '') { @@ -105,26 +109,48 @@ export class SanitiserService { .replace(this.windowsReservedRe, replacement) .replace(this.windowsTrailingRe, replacement); + // Handle case where sanitization results in an empty string + // This can happen with names like "...", "CON", or strings containing only illegal characters + if (sanitized === '') { + sanitized = 'unnamed'; + } + return sanitized; } /** * Truncates a string to a maximum byte length while preserving UTF-8 character integrity + * This method ensures that multi-byte UTF-8 characters are not cut in the middle, + * which would result in invalid UTF-8 sequences. + * * @param str - String to truncate * @param maxBytes - Maximum byte length - * @returns Truncated string + * @returns Truncated string with valid UTF-8 encoding */ private truncateToByteLength(str: string, maxBytes: number): string { const encoder = new TextEncoder(); - const decoder = new TextDecoder('utf-8'); - const encoded = encoder.encode(str); if (encoded.length <= maxBytes) { return str; } - const truncated = encoded.slice(0, maxBytes); - return decoder.decode(truncated); + // Truncate at maxBytes, then work backwards to find a valid UTF-8 boundary + // UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF) + let truncateAt = maxBytes; + while (truncateAt > 0 && (encoded[truncateAt] & 0xC0) === 0x80) { + truncateAt--; + } + + // Decode with fatal mode to ensure we get a valid UTF-8 string + // If decoding fails (which shouldn't happen with our boundary logic), fall back to safe decode + try { + const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }); + return decoder.decode(encoded.slice(0, truncateAt)); + } catch { + // Fallback: use non-fatal mode if something unexpected happens + const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }); + return decoder.decode(encoded.slice(0, truncateAt)); + } } -} +} \ No newline at end of file diff --git a/Services/VaultService.ts b/Services/VaultService.ts index c2215a7..8b04cbe 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -44,7 +44,7 @@ export class VaultService { console.error(`Plugin attempted to access a file that is in the exclusions list: ${filePath}`); return false; } - return this.vault.getAbstractFileByPath(filePath) instanceof TFile; + return this.getAbstractFileByPath(filePath, allowAccessToPluginRoot) instanceof TFile; } public async read(file: TFile, allowAccessToPluginRoot: boolean = false): Promise {