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
This commit is contained in:
Andrew Beal 2025-10-24 17:25:32 +01:00
parent 4a42b1f84c
commit 23721246bd
4 changed files with 43 additions and 14 deletions

View file

@ -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;

View file

@ -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;
}

View file

@ -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));
}
}
}
}

View file

@ -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<string> {