From 423c2654c087e9c23fafe6766ccc7168b6b2dd3a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Mon, 27 Oct 2025 15:43:27 -0400 Subject: Backport pull request #15209 from jellyfin/release-10.11.z Improve symlink handling Original-merge: e5656af1f2e740c6e4f78f613d47d37567940ed8 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index bafe3ad43..959acd475 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -51,8 +51,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule } // Fast path in case the ignore files isn't a symlink and is empty - if ((dirIgnoreFile.Attributes & FileAttributes.ReparsePoint) == 0 - && dirIgnoreFile.Length == 0) + if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0) { return true; } @@ -93,6 +92,12 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { + dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + if (!dirIgnoreFile.Exists) + { + return string.Empty; + } + using (var reader = dirIgnoreFile.OpenText()) { return reader.ReadToEnd(); -- cgit v1.2.3 From 5ea3910af96ead74d9267ec239e47a798a33e78f Mon Sep 17 00:00:00 2001 From: revam Date: Mon, 17 Nov 2025 14:08:47 -0500 Subject: Backport pull request #15263 from jellyfin/release-10.11.z Resolve symlinks for static media source infos Original-merge: 3b2d64995aab63ebaa6832c059a3cc0bdebe90dc Merged-by: crobibero Backported-by: Bond_009 --- .../IO/ManagedFileSystem.cs | 3 +- .../Library/DotIgnoreIgnoreRule.cs | 3 +- .../Trickplay/TrickplayManager.cs | 4 +- MediaBrowser.Controller/Entities/BaseItem.cs | 12 +++- MediaBrowser.Controller/IO/FileSystemHelper.cs | 74 ++++++++++++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) (limited to 'Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs') diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 97e89ca3d..fad97344b 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Security; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -260,7 +261,7 @@ namespace Emby.Server.Implementations.IO { try { - var targetFileInfo = (FileInfo?)fileInfo.ResolveLinkTarget(returnFinalTarget: true); + var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true); if (targetFileInfo is not null) { result.Exists = targetFileInfo.Exists; diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 959acd475..e53502046 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,7 @@ using System; using System.IO; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.IO; @@ -92,7 +93,7 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule private static string GetFileContent(FileInfo dirIgnoreFile) { - dirIgnoreFile = (FileInfo?)dirIgnoreFile.ResolveLinkTarget(returnFinalTarget: true) ?? dirIgnoreFile; + dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; if (!dirIgnoreFile.Exists) { return string.Empty; diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 6f2d2a107..4505a377c 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -254,10 +254,10 @@ public class TrickplayManager : ITrickplayManager } // We support video backdrops, but we should not generate trickplay images for them - var parentDirectory = Directory.GetParent(mediaPath); + var parentDirectory = Directory.GetParent(video.Path); if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase)) { - _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id); + _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", video.Path, video.Id); return; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 4989f0f3f..3c46d53e5 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,6 +24,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaSegments; using MediaBrowser.Controller.Persistence; @@ -1127,6 +1128,15 @@ namespace MediaBrowser.Controller.Entities var protocol = item.PathProtocol; + // Resolve the item path so everywhere we use the media source it will always point to + // the correct path even if symlinks are in use. Calling ResolveLinkTarget on a non-link + // path will return null, so it's safe to check for all paths. + var itemPath = item.Path; + if (protocol is MediaProtocol.File && FileSystemHelper.ResolveLinkTarget(itemPath, returnFinalTarget: true) is { Exists: true } linkInfo) + { + itemPath = linkInfo.FullName; + } + var info = new MediaSourceInfo { Id = item.Id.ToString("N", CultureInfo.InvariantCulture), @@ -1134,7 +1144,7 @@ namespace MediaBrowser.Controller.Entities MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id), Name = GetMediaSourceName(item), - Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path, + Path = enablePathSubstitution ? GetMappedPath(item, itemPath, protocol) : itemPath, RunTimeTicks = item.RunTimeTicks, Container = item.Container, Size = item.Size, diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs index 1a33c3aa8..324aea7e3 100644 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; @@ -61,4 +62,77 @@ public static class FileSystemHelper } } } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The path of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(string linkPath, bool returnFinalTarget = false) + { + // Check if the file exists so the native resolve handler won't throw at us. + if (!File.Exists(linkPath)) + { + return null; + } + + if (!returnFinalTarget) + { + return File.ResolveLinkTarget(linkPath, returnFinalTarget: false) as FileInfo; + } + + if (File.ResolveLinkTarget(linkPath, returnFinalTarget: false) is not FileInfo targetInfo) + { + return null; + } + + var currentPath = targetInfo.FullName; + var visited = new HashSet(StringComparer.Ordinal) { linkPath, currentPath }; + while (File.ResolveLinkTarget(currentPath, returnFinalTarget: false) is FileInfo linkInfo) + { + var targetPath = linkInfo.FullName; + + // If an infinite loop is detected, return the file info for the + // first link in the loop we encountered. + if (!visited.Add(targetPath)) + { + return new FileInfo(targetPath); + } + + targetInfo = linkInfo; + currentPath = targetPath; + + // Exit if the target doesn't exist, so the native resolve handler won't throw at us. + if (!targetInfo.Exists) + { + break; + } + } + + return targetInfo; + } + + /// + /// Gets the target of the specified file link. + /// + /// + /// This helper exists because of this upstream runtime issue; https://github.com/dotnet/runtime/issues/92128. + /// + /// The file info of the file link. + /// true to follow links to the final target; false to return the immediate next link. + /// + /// A if the is a link, regardless of if the target exists; otherwise, null. + /// + public static FileInfo? ResolveLinkTarget(FileInfo fileInfo, bool returnFinalTarget = false) + { + ArgumentNullException.ThrowIfNull(fileInfo); + + return ResolveLinkTarget(fileInfo.FullName, returnFinalTarget); + } } -- cgit v1.2.3 From a08b6ac2664003cb89804ce192f9ae3f3acb4be9 Mon Sep 17 00:00:00 2001 From: CBPJ Date: Mon, 17 Nov 2025 14:09:07 -0500 Subject: Backport pull request #15487 from jellyfin/release-10.11.z Fix gitignore-style not working properly on windows. Original-merge: 435bb14bb266916e9c6f100c4324a94c36126e06 Merged-by: crobibero Backported-by: Bond_009 --- Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index e53502046..46e60dbaa 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -88,6 +89,13 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule var ignore = new Ignore.Ignore(); ignore.Add(ignoreRules); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Mitigate the problem of the Ignore library not handling Windows paths correctly. + // See https://github.com/jellyfin/jellyfin/issues/15484 + return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); + } + return ignore.IsIgnored(fileInfo.FullName); } -- cgit v1.2.3 From 5b3f29946b5628b975578c1ef4a15a3248cd650a Mon Sep 17 00:00:00 2001 From: theguymadmax <171496228+theguymadmax@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:09:09 -0500 Subject: Backport pull request #15501 from jellyfin/release-10.11.z Fix .ignore handling for directories Original-merge: e8150428b62668e062a3432960f98684d3b352cb Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 97 +++++++++------------- 1 file changed, 39 insertions(+), 58 deletions(-) (limited to 'Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 46e60dbaa..473ff8e1d 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Runtime.InteropServices; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -13,28 +12,24 @@ namespace Emby.Server.Implementations.Library; /// public class DotIgnoreIgnoreRule : IResolverIgnoreRule { + private static readonly bool IsWindows = OperatingSystem.IsWindows(); + private static FileInfo? FindIgnoreFile(DirectoryInfo directory) { - var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore")); - if (ignoreFile.Exists) - { - return ignoreFile; - } - - var parentDir = directory.Parent; - if (parentDir is null) + for (var current = directory; current is not null; current = current.Parent) { - return null; + var ignorePath = Path.Join(current.FullName, ".ignore"); + if (File.Exists(ignorePath)) + { + return new FileInfo(ignorePath); + } } - return FindIgnoreFile(parentDir); + return null; } /// - public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) - { - return IsIgnored(fileInfo, parent); - } + public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent); /// /// Checks whether or not the file is ignored. @@ -44,72 +39,58 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule /// True if the file should be ignored. public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent) { - if (fileInfo.IsDirectory) - { - var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName)); - if (dirIgnoreFile is null) - { - return false; - } - - // Fast path in case the ignore files isn't a symlink and is empty - if (dirIgnoreFile.LinkTarget is null && dirIgnoreFile.Length == 0) - { - return true; - } - - // ignore the directory only if the .ignore file is empty - // evaluate individual files otherwise - return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile)); - } + var searchDirectory = fileInfo.IsDirectory + ? new DirectoryInfo(fileInfo.FullName) + : new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty); - var parentDirPath = Path.GetDirectoryName(fileInfo.FullName); - if (string.IsNullOrEmpty(parentDirPath)) + if (string.IsNullOrEmpty(searchDirectory.FullName)) { return false; } - var folder = new DirectoryInfo(parentDirPath); - var ignoreFile = FindIgnoreFile(folder); + var ignoreFile = FindIgnoreFile(searchDirectory); if (ignoreFile is null) { return false; } - string ignoreFileString = GetFileContent(ignoreFile); - - if (string.IsNullOrWhiteSpace(ignoreFileString)) + // Fast path in case the ignore files isn't a symlink and is empty + if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0) { // Ignore directory if we just have the file return true; } + var content = GetFileContent(ignoreFile); + return string.IsNullOrWhiteSpace(content) + || CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory); + } + + private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory) + { // If file has content, base ignoring off the content .gitignore-style rules - var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var ignore = new Ignore.Ignore(); - ignore.Add(ignoreRules); + ignore.Add(rules); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // Mitigate the problem of the Ignore library not handling Windows paths correctly. + // See https://github.com/jellyfin/jellyfin/issues/15484 + var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + + // Add trailing slash for directories to match "folder/" + if (isDirectory) { - // Mitigate the problem of the Ignore library not handling Windows paths correctly. - // See https://github.com/jellyfin/jellyfin/issues/15484 - return ignore.IsIgnored(fileInfo.FullName.NormalizePath('/')); + pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/"); } - return ignore.IsIgnored(fileInfo.FullName); + return ignore.IsIgnored(pathToCheck); } - private static string GetFileContent(FileInfo dirIgnoreFile) + private static string GetFileContent(FileInfo ignoreFile) { - dirIgnoreFile = FileSystemHelper.ResolveLinkTarget(dirIgnoreFile, returnFinalTarget: true) ?? dirIgnoreFile; - if (!dirIgnoreFile.Exists) - { - return string.Empty; - } - - using (var reader = dirIgnoreFile.OpenText()) - { - return reader.ReadToEnd(); - } + ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile; + return ignoreFile.Exists + ? File.ReadAllText(ignoreFile.FullName) + : string.Empty; } } -- cgit v1.2.3 From b9cf26db2f3d219340a17951c289a230d0ccf31a Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 28 Dec 2025 07:22:20 -0500 Subject: Backport pull request #15746 from jellyfin/release-10.11.z Skip invalid ignore rules Original-merge: 6e60634c9f078cc01e343b07a0a6b2a5c230478c Merged-by: crobibero Backported-by: Bond_009 --- .../Library/DotIgnoreIgnoreRule.cs | 48 +++++++++++- .../Library/DotIgnoreIgnoreRuleTest.cs | 87 +++++++++++++++++----- 2 files changed, 115 insertions(+), 20 deletions(-) (limited to 'Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs') diff --git a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs index 473ff8e1d..ef5d24c70 100644 --- a/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text.RegularExpressions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Resolvers; @@ -70,12 +71,55 @@ public class DotIgnoreIgnoreRule : IResolverIgnoreRule { // If file has content, base ignoring off the content .gitignore-style rules var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return CheckIgnoreRules(path, rules, isDirectory); + } + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory) + => CheckIgnoreRules(path, rules, isDirectory, IsWindows); + + /// + /// Checks whether a path should be ignored based on an array of ignore rules. + /// + /// The path to check. + /// The array of ignore rules. + /// Whether the path is a directory. + /// Whether to normalize backslashes to forward slashes (for Windows paths). + /// True if the path should be ignored. + internal static bool CheckIgnoreRules(string path, string[] rules, bool isDirectory, bool normalizePath) + { var ignore = new Ignore.Ignore(); - ignore.Add(rules); + + // Add each rule individually to catch and skip invalid patterns + var validRulesAdded = 0; + foreach (var rule in rules) + { + try + { + ignore.Add(rule); + validRulesAdded++; + } + catch (RegexParseException) + { + // Ignore invalid patterns + } + } + + // If no valid rules were added, fall back to ignoring everything (like an empty .ignore file) + if (validRulesAdded == 0) + { + return true; + } // Mitigate the problem of the Ignore library not handling Windows paths correctly. // See https://github.com/jellyfin/jellyfin/issues/15484 - var pathToCheck = IsWindows ? path.NormalizePath('/') : path; + var pathToCheck = normalizePath ? path.NormalizePath('/') : path; // Add trailing slash for directories to match "folder/" if (isDirectory) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs index d677c9f09..a7bbef7ed 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/DotIgnoreIgnoreRuleTest.cs @@ -1,30 +1,81 @@ +using Emby.Server.Implementations.Library; using Xunit; namespace Jellyfin.Server.Implementations.Tests.Library; public class DotIgnoreIgnoreRuleTest { - [Fact] - public void Test() + private static readonly string[] _rule1 = ["SPs"]; + private static readonly string[] _rule2 = ["SPs", "!thebestshot.mkv"]; + private static readonly string[] _rule3 = ["*.txt", @"{\colortbl;\red255\green255\blue255;}", "videos/", @"\invalid\escape\sequence", "*.mkv"]; + private static readonly string[] _rule4 = [@"{\colortbl;\red255\green255\blue255;}", @"\invalid\escape\sequence"]; + + public static TheoryData CheckIgnoreRulesTestData => + new() + { + // Basic pattern matching + { _rule1, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule1, "cd/sps/ffffff.mkv", false, true }, + { _rule1, "/cd/sps/ffffff.mkv", false, true }, + + // Negate pattern + { _rule2, "f:/cd/sps/ffffff.mkv", false, true }, + { _rule2, "cd/sps/ffffff.mkv", false, true }, + { _rule2, "/cd/sps/ffffff.mkv", false, true }, + { _rule2, "f:/cd/sps/thebestshot.mkv", false, false }, + { _rule2, "cd/sps/thebestshot.mkv", false, false }, + { _rule2, "/cd/sps/thebestshot.mkv", false, false }, + + // Mixed valid and invalid patterns - skips invalid, applies valid + { _rule3, "test.txt", false, true }, + { _rule3, "videos/movie.mp4", false, true }, + { _rule3, "movie.mkv", false, true }, + { _rule3, "test.mp3", false, false }, + + // Only invalid patterns - falls back to ignore all + { _rule4, "any-file.txt", false, true }, + { _rule4, "any/path/to/file.mkv", false, true }, + }; + + public static TheoryData WindowsPathNormalizationTestData => + new() + { + // Windows paths with backslashes - should match when normalizePath is true + { _rule1, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule1, @"D:\media\sps\movie.mkv", false, true }, + { _rule1, @"\\server\share\sps\file.mkv", false, true }, + + // Negate pattern with Windows paths + { _rule2, @"C:\cd\sps\ffffff.mkv", false, true }, + { _rule2, @"C:\cd\sps\thebestshot.mkv", false, false }, + + // Directory matching with Windows paths + { _rule3, @"C:\videos\movie.mp4", false, true }, + { _rule3, @"D:\documents\test.txt", false, true }, + { _rule3, @"E:\music\song.mp3", false, false }, + }; + + [Theory] + [MemberData(nameof(CheckIgnoreRulesTestData))] + public void CheckIgnoreRules_ReturnsExpectedResult(string[] rules, string path, bool isDirectory, bool expectedIgnored) + { + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory)); + } + + [Theory] + [MemberData(nameof(WindowsPathNormalizationTestData))] + public void CheckIgnoreRules_WithWindowsPaths_NormalizesBackslashes(string[] rules, string path, bool isDirectory, bool expectedIgnored) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); + // With normalizePath=true, backslashes should be converted to forward slashes + Assert.Equal(expectedIgnored, DotIgnoreIgnoreRule.CheckIgnoreRules(path, rules, isDirectory, normalizePath: true)); } - [Fact] - public void TestNegatePattern() + [Theory] + [InlineData(@"C:\cd\sps\ffffff.mkv")] + [InlineData(@"D:\media\sps\movie.mkv")] + public void CheckIgnoreRules_WithWindowsPaths_WithoutNormalization_DoesNotMatch(string path) { - var ignore = new Ignore.Ignore(); - ignore.Add("SPs"); - ignore.Add("!thebestshot.mkv"); - Assert.True(ignore.IsIgnored("f:/cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("cd/sps/ffffff.mkv")); - Assert.True(ignore.IsIgnored("/cd/sps/ffffff.mkv")); - Assert.True(!ignore.IsIgnored("f:/cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("cd/sps/thebestshot.mkv")); - Assert.True(!ignore.IsIgnored("/cd/sps/thebestshot.mkv")); + // Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes + Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false)); } } -- cgit v1.2.3