diff --git a/docs/specs/SPEC-noise-file-detection.md b/docs/specs/SPEC-noise-file-detection.md new file mode 100644 index 000000000..0d56cabf7 --- /dev/null +++ b/docs/specs/SPEC-noise-file-detection.md @@ -0,0 +1,33 @@ +# Noise File Detection Specification + +## Purpose + +This document lists the known noise file names filtered by the client inventory pipeline and their platform origin. + +The runtime source of truth is the embedded JSON resource: +`src/ByteSync.Client/Services/Inventories/noise-files.json`. + +## Known noise file names + +| File name | Origin platform | Typical purpose | +| --- | --- | --- | +| `desktop.ini` | Windows | Folder customization metadata | +| `thumbs.db` | Windows | Thumbnail cache | +| `ehthumbs.db` | Windows | Media Center thumbnail cache | +| `ehthumbs_vista.db` | Windows | Vista Media Center thumbnail cache | +| `.desktop.ini` | Windows/Linux legacy compatibility | Legacy hidden variant | +| `.thumbs.db` | Windows/Linux legacy compatibility | Legacy hidden variant | +| `.DS_Store` | macOS | Finder metadata | +| `.AppleDouble` | macOS | Resource fork metadata | +| `.LSOverride` | macOS | Launch Services overrides | +| `.Spotlight-V100` | macOS | Spotlight indexing data | +| `.Trashes` | macOS | Trash metadata or folder marker | +| `.fseventsd` | macOS | File system event metadata | +| `.TemporaryItems` | macOS | Temporary items marker | +| `.VolumeIcon.icns` | macOS | Custom volume icon | +| `.directory` | Linux (KDE) | Directory display metadata | + +## Matching behavior + +- On Linux, matching is case-sensitive. +- On non-Linux platforms, matching is case-insensitive. diff --git a/src/ByteSync.Client/ByteSync.Client.csproj b/src/ByteSync.Client/ByteSync.Client.csproj index 25d640f9f..818262957 100644 --- a/src/ByteSync.Client/ByteSync.Client.csproj +++ b/src/ByteSync.Client/ByteSync.Client.csproj @@ -111,6 +111,7 @@ + @@ -258,4 +259,4 @@ Code - \ No newline at end of file + diff --git a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs index 213add22f..47e33b74f 100644 --- a/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs +++ b/src/ByteSync.Client/Services/Inventories/FileSystemInspector.cs @@ -63,13 +63,7 @@ public bool IsSystemAttribute(FileInfo fileInfo) public bool IsNoiseFileName(FileInfo fileInfo, OSPlatforms os) { - var comparison = os == OSPlatforms.Linux ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; - - return fileInfo.Name.Equals("desktop.ini", comparison) - || fileInfo.Name.Equals("thumbs.db", comparison) - || fileInfo.Name.Equals(".desktop.ini", comparison) - || fileInfo.Name.Equals(".thumbs.db", comparison) - || fileInfo.Name.Equals(".DS_Store", comparison); + return NoiseFileDetector.IsNoiseFileName(fileInfo.Name, os); } public bool IsReparsePoint(FileSystemInfo fsi) @@ -120,4 +114,4 @@ private bool SafeIsReparsePoint(FileSystemInfo fsi) return false; } } -} \ No newline at end of file +} diff --git a/src/ByteSync.Client/Services/Inventories/NoiseFileDetector.cs b/src/ByteSync.Client/Services/Inventories/NoiseFileDetector.cs new file mode 100644 index 000000000..bba9cf567 --- /dev/null +++ b/src/ByteSync.Client/Services/Inventories/NoiseFileDetector.cs @@ -0,0 +1,44 @@ +using System.Reflection; +using System.Text.Json; +using ByteSync.Common.Business.Misc; + +namespace ByteSync.Services.Inventories; + +public static class NoiseFileDetector +{ + private const string NoiseFileResourceSuffix = ".Services.Inventories.noise-files.json"; + private static readonly string[] KnownNoiseFileNames = LoadNoiseFileNames(); + + private static readonly HashSet CaseSensitiveNoiseFileNames = new(KnownNoiseFileNames, StringComparer.Ordinal); + private static readonly HashSet CaseInsensitiveNoiseFileNames = new(KnownNoiseFileNames, StringComparer.OrdinalIgnoreCase); + + public static bool IsNoiseFileName(string? fileName, OSPlatforms os) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return false; + } + + return os == OSPlatforms.Linux + ? CaseSensitiveNoiseFileNames.Contains(fileName) + : CaseInsensitiveNoiseFileNames.Contains(fileName); + } + + private static string[] LoadNoiseFileNames() + { + var assembly = typeof(NoiseFileDetector).Assembly; + var resourceName = assembly.GetManifestResourceNames() + .Single(rn => rn.EndsWith(NoiseFileResourceSuffix, StringComparison.Ordinal)); + + using var stream = assembly.GetManifestResourceStream(resourceName); + ArgumentNullException.ThrowIfNull(stream); + + var parsed = JsonSerializer.Deserialize(stream); + ArgumentNullException.ThrowIfNull(parsed); + + return parsed + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } +} diff --git a/src/ByteSync.Client/Services/Inventories/noise-files.json b/src/ByteSync.Client/Services/Inventories/noise-files.json new file mode 100644 index 000000000..22093319c --- /dev/null +++ b/src/ByteSync.Client/Services/Inventories/noise-files.json @@ -0,0 +1,17 @@ +[ + "desktop.ini", + "thumbs.db", + "ehthumbs.db", + "ehthumbs_vista.db", + ".desktop.ini", + ".thumbs.db", + ".DS_Store", + ".AppleDouble", + ".LSOverride", + ".Spotlight-V100", + ".Trashes", + ".fseventsd", + ".TemporaryItems", + ".VolumeIcon.icns", + ".directory" +] diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs index 618a7fcc6..f69d72c26 100644 --- a/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/FileSystemInspectorTests.cs @@ -1,4 +1,5 @@ using ByteSync.Business.Inventories; +using ByteSync.Common.Business.Misc; using ByteSync.Interfaces.Controls.Inventories; using ByteSync.Services.Inventories; using FluentAssertions; @@ -131,4 +132,46 @@ public void ClassifyEntry_FallsBackToRegularFile_WhenPosixClassifierThrows() Directory.Delete(tempDirectory.FullName, true); } } -} \ No newline at end of file + + [Test] + public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFile() + { + var inspector = new FileSystemInspector(); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var filePath = Path.Combine(tempDirectory.FullName, "thumbs.db"); + File.WriteAllText(filePath, "x"); + var fileInfo = new FileInfo(filePath); + + try + { + var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); + + result.Should().BeTrue(); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } + + [Test] + public void IsNoiseFileName_ShouldReturnFalse_ForUnknownFile() + { + var inspector = new FileSystemInspector(); + var tempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + var filePath = Path.Combine(tempDirectory.FullName, "regular.txt"); + File.WriteAllText(filePath, "x"); + var fileInfo = new FileInfo(filePath); + + try + { + var result = inspector.IsNoiseFileName(fileInfo, OSPlatforms.Windows); + + result.Should().BeFalse(); + } + finally + { + Directory.Delete(tempDirectory.FullName, true); + } + } +} diff --git a/tests/ByteSync.Client.UnitTests/Services/Inventories/NoiseFileDetectorTests.cs b/tests/ByteSync.Client.UnitTests/Services/Inventories/NoiseFileDetectorTests.cs new file mode 100644 index 000000000..80fb9d1ec --- /dev/null +++ b/tests/ByteSync.Client.UnitTests/Services/Inventories/NoiseFileDetectorTests.cs @@ -0,0 +1,111 @@ +using System.Text.Json; +using ByteSync.Common.Business.Misc; +using ByteSync.Services.Inventories; +using FluentAssertions; +using NUnit.Framework; + +namespace ByteSync.Client.UnitTests.Services.Inventories; + +public class NoiseFileDetectorTests +{ + private static readonly string[] KnownNoiseFileNames = LoadNoiseFileNamesFromEmbeddedResource(); + + [TestCaseSource(nameof(KnownNoiseFileNames))] + public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFiles_OnWindows(string fileName) + { + var result = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Windows); + + result.Should().BeTrue(); + } + + [TestCaseSource(nameof(KnownNoiseFileNames))] + public void IsNoiseFileName_ShouldReturnTrue_ForKnownNoiseFiles_OnLinux(string fileName) + { + var result = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Linux); + + result.Should().BeTrue(); + } + + [TestCase("DESKTOP.INI")] + [TestCase("THUMBS.DB")] + [TestCase("EHTHUMBS.DB")] + [TestCase("EHTHUMBS_VISTA.DB")] + [TestCase(".ds_store")] + [TestCase(".appledouble")] + [TestCase(".lsoverride")] + [TestCase(".spotlight-v100")] + [TestCase(".trashes")] + [TestCase(".FSEVENTSD")] + [TestCase(".temporaryitems")] + [TestCase(".volumeicon.icns")] + [TestCase(".DIRECTORY")] + public void IsNoiseFileName_ShouldBeCaseInsensitive_OnNonLinuxPlatforms(string fileName) + { + var windowsResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Windows); + var macResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.MacOs); + + windowsResult.Should().BeTrue(); + macResult.Should().BeTrue(); + } + + [TestCase("DESKTOP.INI")] + [TestCase("THUMBS.DB")] + [TestCase("EHTHUMBS.DB")] + [TestCase("EHTHUMBS_VISTA.DB")] + [TestCase(".ds_store")] + [TestCase(".appledouble")] + [TestCase(".lsoverride")] + [TestCase(".spotlight-v100")] + [TestCase(".trashes")] + [TestCase(".FSEVENTSD")] + [TestCase(".temporaryitems")] + [TestCase(".volumeicon.icns")] + [TestCase(".DIRECTORY")] + public void IsNoiseFileName_ShouldBeCaseSensitive_OnLinux(string fileName) + { + var result = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Linux); + + result.Should().BeFalse(); + } + + [TestCase("readme.md")] + [TestCase("normal.txt")] + [TestCase(".gitignore")] + public void IsNoiseFileName_ShouldReturnFalse_ForUnknownFileNames(string fileName) + { + var windowsResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Windows); + var linuxResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Linux); + + windowsResult.Should().BeFalse(); + linuxResult.Should().BeFalse(); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void IsNoiseFileName_ShouldReturnFalse_ForEmptyValues(string? fileName) + { + var windowsResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Windows); + var linuxResult = NoiseFileDetector.IsNoiseFileName(fileName, OSPlatforms.Linux); + + windowsResult.Should().BeFalse(); + linuxResult.Should().BeFalse(); + } + + private static string[] LoadNoiseFileNamesFromEmbeddedResource() + { + var assembly = typeof(NoiseFileDetector).Assembly; + var resourceName = assembly.GetManifestResourceNames() + .SingleOrDefault(rn => rn.EndsWith(".Services.Inventories.noise-files.json", StringComparison.Ordinal)); + + resourceName.Should().NotBeNull(); + + using var stream = assembly.GetManifestResourceStream(resourceName!); + stream.Should().NotBeNull(); + + var data = JsonSerializer.Deserialize(stream!); + data.Should().NotBeNull(); + + return data!; + } +} \ No newline at end of file