Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions CatalogPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -14,10 +14,23 @@ public class CatalogPlugin : PluginBase

public static void Process(CatalogOptions options)
{
//Validate and display arguments
var currentDirectory = Directory.GetCurrentDirectory();
var fileEntries = Directory.GetFiles(currentDirectory, options.InputPath, SearchOption.AllDirectories);
string[] fileEntries = { };

//Absolute path
if (Path.IsPathFullyQualified(options.InputPath))
{
string path = options.InputPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');

if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
Comment on lines +23 to +26

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The absolute path handling lacks validation for path traversal attacks. An absolute path could potentially include ".." sequences that allow accessing files outside intended directories. Consider using Path.GetFullPath to normalize and validate the path before processing.

Suggested change
string path = options.InputPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
string fullPath;
try
{
// Normalize the path to eliminate any relative segments
fullPath = Path.GetFullPath(options.InputPath);
}
catch (Exception)
{
WriteError($"Input path {options.InputPath} is invalid.");
return;
}
// Prevent directory traversal via parent-directory segments
if (options.InputPath.Contains(".."))
{
WriteError("Parent directory segments (..) are not allowed in the input path.");
return;
}
string path = fullPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');
if (File.Exists(fullPath) || Directory.Exists(fullPath))

Copilot uses AI. Check for mistakes.
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
Comment on lines +23 to +27

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path normalization using Replace on line 23 is used to find the last path separator, but the original options.InputPath is used for File.Exists and Directory.Exists checks on line 26. This creates an inconsistency where the normalized path is parsed but the original path is checked for existence. If the original path uses backslashes and refers to a directory, the logic may fail to correctly identify the directory vs pattern components.

Suggested change
string path = options.InputPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
string directoryPath;
string searchPattern;
// If the input path is an existing directory, search for all .txt files under it.
if (Directory.Exists(options.InputPath))
{
directoryPath = options.InputPath;
searchPattern = "*.txt";
}
else
{
directoryPath = Path.GetDirectoryName(options.InputPath) ?? string.Empty;
searchPattern = Path.GetFileName(options.InputPath);
}
if (!string.IsNullOrEmpty(directoryPath) &&
!string.IsNullOrEmpty(searchPattern) &&
Directory.Exists(directoryPath))
{
fileEntries = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories);
}

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When File.Exists or Directory.Exists returns false on line 26, the condition fails but fileEntries remains as an empty array without any error message or alternative handling. This silent failure makes debugging difficult for users who provide an absolute path that doesn't exist, as they won't get the "No .txt files found" error until line 34, which may be misleading.

Suggested change
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
{
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
}
else
{
WriteError($"Input path {options.InputPath} was not found.");
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path splitting logic doesn't correctly handle all absolute path scenarios. If InputPath is an absolute directory path like "C:\Users\data" (without a wildcard pattern), this code splits it into directory="C:\Users" and pattern="data", which will search for files matching "data" pattern in "C:\Users" instead of searching all files in "C:\Users\data". The logic should check if InputPath is a directory and handle it differently from a file pattern.

Suggested change
string path = options.InputPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
// If the input is an existing directory, search all files under it.
if (Directory.Exists(options.InputPath))
{
fileEntries = Directory.GetFiles(options.InputPath, "*", SearchOption.AllDirectories);
}
// If the input is an existing file, process just that file.
else if (File.Exists(options.InputPath))
{
fileEntries = new[] { options.InputPath };
}
// Otherwise treat the input as an absolute path containing a file pattern.
else
{
var directory = Path.GetDirectoryName(options.InputPath);
var pattern = Path.GetFileName(options.InputPath);
if (string.IsNullOrEmpty(directory))
{
directory = currentDirectory;
}
fileEntries = Directory.GetFiles(directory, pattern, SearchOption.AllDirectories);
}

Copilot uses AI. Check for mistakes.
}
//Relative path
else
fileEntries = Directory.GetFiles(currentDirectory, options.InputPath, SearchOption.AllDirectories);

Comment on lines +23 to 32

Copilot AI Jan 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for handling absolute paths has several issues. First, if the path doesn't exist (line 26 check fails), fileEntries will remain as an empty array but execution continues, which differs from the relative path behavior. Second, the path splitting logic assumes the InputPath contains at least one path separator, but a fully qualified path like "C:" would cause LastIndexOf to return -1, leading to incorrect range operations on line 27. Third, if InputPath is a directory path (not a file pattern), the split logic will incorrectly separate the directory name from its parent.

Suggested change
string path = options.InputPath.Replace("\\", "/");
int pos = path.LastIndexOf('/');
if (File.Exists(options.InputPath) || Directory.Exists(options.InputPath))
fileEntries = Directory.GetFiles(path[..pos], path[(pos + 1)..], SearchOption.AllDirectories);
}
//Relative path
else
fileEntries = Directory.GetFiles(currentDirectory, options.InputPath, SearchOption.AllDirectories);
// If it's an existing file, just process that single file
if (File.Exists(options.InputPath))
{
fileEntries = new[] { options.InputPath };
}
// If it's an existing directory, search all files within it
else if (Directory.Exists(options.InputPath))
{
fileEntries = Directory.GetFiles(options.InputPath, "*", SearchOption.AllDirectories);
}
else
{
// Treat as a pattern with directory and file mask components
var directoryPart = Path.GetDirectoryName(options.InputPath);
var patternPart = Path.GetFileName(options.InputPath);
// If there is no directory part, the pattern is not rooted anywhere valid
if (string.IsNullOrEmpty(directoryPart))
{
WriteError($"Input path {options.InputPath} was not found.");
return;
}
if (!Directory.Exists(directoryPart))
{
WriteError($"Input folder {directoryPart} was not found.");
return;
}
// If no explicit pattern, default to all files
if (string.IsNullOrEmpty(patternPart))
{
patternPart = "*";
}
fileEntries = Directory.GetFiles(directoryPart, patternPart, SearchOption.AllDirectories);
}
}
//Relative path
else
{
fileEntries = Directory.GetFiles(currentDirectory, options.InputPath, SearchOption.AllDirectories);
}

Copilot uses AI. Check for mistakes.
//Validate and display arguments
if (fileEntries.Length == 0)
{
WriteError($"No .txt files found for {options.InputPath}");
Expand Down Expand Up @@ -124,7 +137,7 @@ public static void Process(CatalogOptions options)
}
}

#pragma warning disable SYSLIB0021
#pragma warning disable SYSLIB0021
//We keep using Sha1Managed for performance reasons
using (var sha1 = new SHA1Managed())
{
Expand Down Expand Up @@ -268,7 +281,7 @@ private static async Task DoXReference(CatalogOptions options)
{
//Create a new lock object for this hex key
_locks.Add($"{hex1}{hex2}", new SemaphoreSlim(1, 1));
}
}
}

//Loop through each file with this prefix in the output folder
Expand Down Expand Up @@ -300,7 +313,7 @@ private static async Task DoXReference(CatalogOptions options)
tasks.Remove(completedTask);
}
}

//We now have 256 files full of associated words, a word can appear multiple times, but only in one file
//Loop through each file, combine entries, then optimise the file
bucketCount = 0;
Expand Down Expand Up @@ -328,7 +341,7 @@ private static async Task DoXReference(CatalogOptions options)
WriteProgress($"Optimising files", bucketCount, 256);

tasks.Remove(completedTask);
}
}
}
}

Expand Down Expand Up @@ -436,7 +449,7 @@ private static async Task WriteFiles(Dictionary<string, Dictionary<string, int>>
{
var output = new Dictionary<string, List<string>>();

#pragma warning disable SYSLIB0021
#pragma warning disable SYSLIB0021
//We keep using Sha1Managed for performance reasons
using (var sha1 = new SHA1Managed())
{
Expand Down Expand Up @@ -509,7 +522,7 @@ private static async Task WriteFiles(Dictionary<string, Dictionary<string, int>>
//When the task is ready, always release the semaphore.
_locks[de.Key].Release();
}
}
}
}

//Sort by key, and optimise key/words
Expand Down
Loading