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