From 2804f2b4a9765910069a633d6d90689b1387fbc2 Mon Sep 17 00:00:00 2001 From: miclat97 <40289683+miclat97@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:11:20 +0000 Subject: [PATCH] feat: add support for displaying Alternate Data Streams Added a configuration option (enabled by default) to display NTFS Alternate Data Streams (ADS) alongside normal files. Streams are enumerated via P/Invoke calls to kernel32.dll and rendered as text by default. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- QuickViewFile/Helpers/AdsHelper.cs | 85 +++++++++++++++++++ QuickViewFile/Helpers/ConfigHelper.cs | 2 + QuickViewFile/Models/ConfigModel.cs | 1 + QuickViewFile/SettingsWindow.xaml.cs | 5 +- QuickViewFile/ViewModel/FilesListViewModel.cs | 37 +++++++- 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 QuickViewFile/Helpers/AdsHelper.cs diff --git a/QuickViewFile/Helpers/AdsHelper.cs b/QuickViewFile/Helpers/AdsHelper.cs new file mode 100644 index 0000000..7e2599a --- /dev/null +++ b/QuickViewFile/Helpers/AdsHelper.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace QuickViewFile.Helpers +{ + public static class AdsHelper + { + [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr FindFirstStreamW(string lpFileName, STREAM_INFO_LEVELS InfoLevel, out WIN32_FIND_STREAM_DATA lpFindStreamData, uint dwFlags); + + [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FindNextStreamW(IntPtr hFindStream, out WIN32_FIND_STREAM_DATA lpFindStreamData); + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FindClose(IntPtr hFindFile); + + private enum STREAM_INFO_LEVELS + { + FindStreamInfoStandard, + FindStreamInfoMaxInfoLevel + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WIN32_FIND_STREAM_DATA + { + public long StreamSize; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 296)] + public string cStreamName; + } + + public class StreamInfo + { + public string Name { get; set; } = string.Empty; + public long Size { get; set; } + } + + public static List GetAlternateDataStreams(string filePath) + { + List result = new List(); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return result; + } + + WIN32_FIND_STREAM_DATA findStreamData; + IntPtr hFind = FindFirstStreamW(filePath, STREAM_INFO_LEVELS.FindStreamInfoStandard, out findStreamData, 0); + + if (hFind != new IntPtr(-1) && hFind != IntPtr.Zero) + { + try + { + do + { + if (findStreamData.cStreamName != "::$DATA") + { + string streamName = findStreamData.cStreamName; + if (streamName.StartsWith(":")) + { + streamName = streamName.Substring(1); + int dataIndex = streamName.IndexOf(":$DATA", StringComparison.OrdinalIgnoreCase); + if (dataIndex >= 0) + { + streamName = streamName.Substring(0, dataIndex); + } + } + if (!string.IsNullOrEmpty(streamName)) + { + result.Add(new StreamInfo { Name = streamName, Size = findStreamData.StreamSize }); + } + } + } + while (FindNextStreamW(hFind, out findStreamData)); + } + finally + { + FindClose(hFind); + } + } + return result; + } + } +} diff --git a/QuickViewFile/Helpers/ConfigHelper.cs b/QuickViewFile/Helpers/ConfigHelper.cs index e93318d..28136bf 100644 --- a/QuickViewFile/Helpers/ConfigHelper.cs +++ b/QuickViewFile/Helpers/ConfigHelper.cs @@ -47,6 +47,7 @@ public static void SaveConfig(ConfigModel config) key.SetValue(nameof(ConfigModel.ShadowEffect), config.ShadowEffect); key.SetValue(nameof(ConfigModel.ThemeMode), config.ThemeMode); key.SetValue(nameof(ConfigModel.ShadowQuality), config.ShadowQuality); + key.SetValue(nameof(ConfigModel.ShowAlternateDataStreams), config.ShowAlternateDataStreams); key.SetValue(nameof(ConfigModel.ShadowDepth), config.ShadowDepth); key.SetValue(nameof(ConfigModel.ShadowOpacity), config.ShadowOpacity); key.SetValue(nameof(ConfigModel.ShadowBlur), config.ShadowBlur); @@ -96,6 +97,7 @@ public static ConfigModel LoadConfig() config.ShadowEffect = (int)key.GetValue(nameof(ConfigModel.ShadowEffect), config.ShadowEffect); config.ShadowQuality = (int)key.GetValue(nameof(ConfigModel.ShadowQuality), config.ShadowQuality); config.ThemeMode = (int)key.GetValue(nameof(ConfigModel.ThemeMode), config.ThemeMode); + config.ShowAlternateDataStreams = (int)key.GetValue(nameof(ConfigModel.ShowAlternateDataStreams), config.ShowAlternateDataStreams); config.Utf8InsteadOfASCIITextPreview = (int)key.GetValue(nameof(ConfigModel.Utf8InsteadOfASCIITextPreview), config.Utf8InsteadOfASCIITextPreview); config.ShadowDepth = double.Parse((string)key.GetValue(nameof(ConfigModel.ShadowDepth).ToString(), config.ShadowDepth)); config.ShadowOpacity = double.Parse((string)key.GetValue(nameof(ConfigModel.ShadowOpacity).ToString(), config.ShadowOpacity)); diff --git a/QuickViewFile/Models/ConfigModel.cs b/QuickViewFile/Models/ConfigModel.cs index 6c05761..0a58ffa 100644 --- a/QuickViewFile/Models/ConfigModel.cs +++ b/QuickViewFile/Models/ConfigModel.cs @@ -51,6 +51,7 @@ public string BitmapScalingMode public int RenderMode { get; set; } = 0; // 0 - Default, 1 - SoftwareOnly public int EdgeMode { get; set; } = 0; // 0 - Unspecified, 1 - Aliased public int ThemeMode { get; set; } = 0; // 0 - System, 1 - Light, 2 - Dark + public int ShowAlternateDataStreams { get; set; } = 1; // 0 - Disabled, 1 - Enabled public double ShadowDepth { get; set; } = 0; public double ShadowOpacity { get; set; } = 0; public double ShadowBlur { get; set; } = 0; diff --git a/QuickViewFile/SettingsWindow.xaml.cs b/QuickViewFile/SettingsWindow.xaml.cs index 5793a5c..6527661 100644 --- a/QuickViewFile/SettingsWindow.xaml.cs +++ b/QuickViewFile/SettingsWindow.xaml.cs @@ -63,11 +63,11 @@ private void GenerateUI() panel.Children.Add(comboBox); } else if (prop.Name == "ShadowEffect" || prop.Name == "ShadowQuality" || prop.Name == "RenderMode" || - prop.Name == "EdgeMode" || prop.Name == "Utf8InsteadOfASCIITextPreview") + prop.Name == "EdgeMode" || prop.Name == "Utf8InsteadOfASCIITextPreview" || prop.Name == "ShowAlternateDataStreams") { var comboBox = new ComboBox { Width = 400, VerticalAlignment = VerticalAlignment.Center }; - if (prop.Name == "Utf8InsteadOfASCIITextPreview" || prop.Name == "ShadowEffect") + if (prop.Name == "Utf8InsteadOfASCIITextPreview" || prop.Name == "ShadowEffect" || prop.Name == "ShowAlternateDataStreams") { comboBox.Items.Add(new ComboBoxItem { Content = "Disabled", Tag = 0 }); comboBox.Items.Add(new ComboBoxItem { Content = "Enabled", Tag = 1 }); @@ -144,6 +144,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) ConfigHelper.loadedConfig.RenderMode = _config.RenderMode; ConfigHelper.loadedConfig.EdgeMode = _config.EdgeMode; ConfigHelper.loadedConfig.ThemeMode = _config.ThemeMode; + ConfigHelper.loadedConfig.ShowAlternateDataStreams = _config.ShowAlternateDataStreams; ConfigHelper.loadedConfig.ShadowDepth = _config.ShadowDepth; ConfigHelper.loadedConfig.ShadowOpacity = _config.ShadowOpacity; ConfigHelper.loadedConfig.ShadowBlur = _config.ShadowBlur; diff --git a/QuickViewFile/ViewModel/FilesListViewModel.cs b/QuickViewFile/ViewModel/FilesListViewModel.cs index 59c0591..10d58e9 100644 --- a/QuickViewFile/ViewModel/FilesListViewModel.cs +++ b/QuickViewFile/ViewModel/FilesListViewModel.cs @@ -171,6 +171,25 @@ public void RefreshFiles(string? fileToSelect = null) LastModifiedString = file.LastWriteTime.ToString("yyyy-MM-dd HH:mm"), FileContentModel = new FileContentModel() }); + + if (Config.ShowAlternateDataStreams == 1) + { + var streams = AdsHelper.GetAlternateDataStreams(file.FullName); + foreach (var stream in streams) + { + ActiveListItems.Add(new ItemList + { + Name = $"{file.Name}:{stream.Name}", + Size = Math.Round((stream.Size / 1024.0), MidpointRounding.ToPositiveInfinity).ToString(), + SizeBytes = stream.Size, + FullPath = $"{file.FullName}:{stream.Name}", + IsDirectory = false, + LastModified = file.LastWriteTime, + LastModifiedString = file.LastWriteTime.ToString("yyyy-MM-dd HH:mm"), + FileContentModel = new FileContentModel() + }); + } + } } try @@ -243,14 +262,28 @@ public async Task LazyLoadFile(bool? forceLoad = false) return; string filePath = SelectedItem.FullPath; - string ext = Path.GetExtension(filePath).ToLowerInvariant(); + string extensionPath = filePath; + + string fileName = Path.GetFileName(filePath); + bool isAds = false; + if (fileName != null && fileName.Contains(':')) + { + int colonIndex = extensionPath.LastIndexOf(':'); + if (colonIndex > 3) // More than drive letter like C:\ + { + extensionPath = extensionPath.Substring(0, colonIndex); + isAds = true; + } + } + + string ext = Path.GetExtension(extensionPath).ToLowerInvariant(); FileTypeEnum fileType = FileTypeEnum.Text; var imageExtensions = ConfigHelper.GetStringsFromCommaSeparatedString(Config.ImageExtensions); var musicExtensions = ConfigHelper.GetStringsFromCommaSeparatedString(Config.MusicExtensions); var videoExtension = ConfigHelper.GetStringsFromCommaSeparatedString(Config.VideoExtensions); var liveStreamExtensions = ConfigHelper.GetStringsFromCommaSeparatedString(Config.LiveStreamExtensions); - if (ext != null) + if (ext != null && !isAds) // Default to Text for ADS because many video/image formats cannot be streamed directly from ADS { if (liveStreamExtensions.Contains(ext)) {