files-community / Files

A modern file manager that helps users organize their files and folders.
https://files.community
MIT License
34.8k stars 2.21k forks source link

Feature: Show ADS (alternative data streams) preview in file properties #15078

Open BiosNod opened 8 months ago

BiosNod commented 8 months ago

What feature or improvement do you think would benefit Files?

Hello, I know there is already a "Show alternate data streams" option in Files, but it shows all ADS nearby file, but it seems more useful to show ADS fields in file properties (like hashes).

Requirements

Files Version

3.3.0.0

Windows Version

Windows 10 22H2 19045.3930

Comments

No response

Josh65-2201 commented 8 months ago

Thanks for the feedback, How would it be more useful? They show in the normal file view as they can be edited like a file

BiosNod commented 8 months ago

How would this be more useful?

I have 10-15 ADS for each file and I only want to see the ADS in the properties windows and not in the window with all the files because...It turns into a huge mess of files mixed with ADS, This is probably a very rare case, but it does happen :D

For example I have ADS: checkStatus, checkDate, hashMD5 and more other

BiosNod commented 8 months ago

But all my ADS contain ASCII text and no binary data, so these fields can be displayed as a table "ADS-name | ADS-content". But I don't know how to skip showing the ADS field in case some ADS have binary data instead of plain text (maybe just skip showing the ADS field in case >100 bytes...), it may not be possible to determine which ADS have plain text and what binary data (must be skipped)

yaira2 commented 3 weeks ago

How would this work for streams with multiple lines of text?

BiosNod commented 3 weeks ago

How would this work for streams with multiple lines of text?

Show only first ~50 symbols in one row, I haven't multi line data in ADS, something like this:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

public class AlternateStreamReader
{
    // Dictionary to store stream names and their contents
    private Dictionary<string, string> _streams = new Dictionary<string, string>();

    // Maximum content length before truncation
    private const int MAX_CONTENT_LENGTH = 50;

    // Regular expression pattern for detecting binary content
    // This pattern looks for common binary characters
    private static readonly Regex BinaryPattern = new Regex(
        "[\x00-\x08\x0B\x0C\x0E-\x1F]",
        RegexOptions.Compiled
    );

    /// <summary>
    /// Reads all alternate data streams for a given file
    /// </summary>
    /// <param name="filePath">Full path to the file to analyze</param>
    /// <returns>Dictionary with stream names as keys and their content as values</returns>
    public Dictionary<string, string> ReadAlternateStreams(string filePath)
    {
        _streams.Clear();

        // Check if file exists
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("The specified file was not found.", filePath);
        }

        // Get all streams for the file
        var streamInfos = Directory.GetFiles(filePath + ":*");

        foreach (var streamPath in streamInfos)
        {
            try
            {
                // Extract stream name from the full path
                // Format is "filename.ext:streamname:$DATA"
                string streamName = ExtractStreamName(streamPath);

                // Read stream content
                string content = ReadStreamContent(streamPath);

                // Add to dictionary if stream name was successfully extracted
                if (!string.IsNullOrEmpty(streamName))
                {
                    _streams[streamName] = content;
                }
            }
            catch (Exception ex)
            {
                // Log or handle the error as needed
                Console.WriteLine($"Error reading stream {streamPath}: {ex.Message}");
            }
        }

        return _streams;
    }

    /// <summary>
    /// Extracts the stream name from the full stream path
    /// </summary>
    /// <param name="streamPath">Full path including the stream name</param>
    /// <returns>Stream name without the :$DATA suffix</returns>
    private string ExtractStreamName(string streamPath)
    {
        // Find the position of the first colon (after drive letter)
        int firstColon = streamPath.IndexOf(':', 2);
        if (firstColon == -1) return string.Empty;

        // Extract everything between the first colon and :$DATA
        string streamPart = streamPath.Substring(firstColon + 1);
        return streamPart.Replace(":$DATA", "");
    }

    /// <summary>
    /// Reads and processes the content of a stream
    /// </summary>
    /// <param name="streamPath">Full path to the stream</param>
    /// <returns>Processed stream content</returns>
    private string ReadStreamContent(string streamPath)
    {
        // Read all bytes from the stream
        byte[] content = File.ReadAllBytes(streamPath);

        // If content is empty, return empty string
        if (content.Length == 0) return string.Empty;

        // Try to convert to string using UTF8
        string stringContent;
        try
        {
            stringContent = Encoding.UTF8.GetString(content);

            // Check if content appears to be binary
            if (IsBinaryContent(content, stringContent))
            {
                return "<binary data>";
            }
        }
        catch
        {
            // If conversion fails, assume binary data
            return "<binary data>";
        }

        // Truncate if necessary
        if (stringContent.Length > MAX_CONTENT_LENGTH)
        {
            return stringContent.Substring(0, MAX_CONTENT_LENGTH) + "...";
        }

        return stringContent;
    }

    /// <summary>
    /// Attempts to determine if content is binary
    /// </summary>
    /// <param name="bytes">Raw byte content</param>
    /// <param name="stringContent">Content converted to string</param>
    /// <returns>True if content appears to be binary</returns>
    private bool IsBinaryContent(byte[] bytes, string stringContent)
    {
        // Check for null bytes (common in binary files)
        if (Array.IndexOf(bytes, (byte)0) != -1)
            return true;

        // Check for binary characters using regex
        if (BinaryPattern.IsMatch(stringContent))
            return true;

        // Additional heuristic: check ratio of printable to non-printable characters
        int nonPrintable = 0;
        foreach (char c in stringContent)
        {
            if (char.IsControl(c) && !char.IsWhiteSpace(c))
                nonPrintable++;
        }

        // If more than 10% non-printable characters, consider it binary
        return (double)nonPrintable / stringContent.Length > 0.1;
    }
}

// Example usage:
public class Program
{
    public static void Main()
    {
        var reader = new AlternateStreamReader();
        try
        {
            var streams = reader.ReadAlternateStreams(@"C:\path\to\your\file.txt");
            foreach (var stream in streams)
            {
                Console.WriteLine($"Stream: {stream.Key}");
                Console.WriteLine($"Content: {stream.Value}");
                Console.WriteLine();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

And when user click on "eye" button nearby one row - he will see a modal window with first 1000 bytes as a plain text or as a HEX view if data is binary:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

public class AlternateStreamReader
{
    // Dictionary to store stream names and their contents
    private Dictionary<string, StreamContent> _streams = new Dictionary<string, StreamContent>();

    // Maximum content length for plain text display
    private const int MAX_CONTENT_LENGTH = 50;

    // Maximum bytes to show in hex view
    private const int MAX_HEX_BYTES = 1000;

    // Number of bytes per line in hex view
    private const int BYTES_PER_LINE = 16;

    // Regular expression pattern for detecting binary content
    private static readonly Regex BinaryPattern = new Regex(
        "[\x00-\x08\x0B\x0C\x0E-\x1F]",
        RegexOptions.Compiled
    );

    /// <summary>
    /// Class to store stream content details
    /// </summary>
    public class StreamContent
    {
        public bool IsBinary { get; set; }
        public string DisplayContent { get; set; }
        public string HexView { get; set; }
        public byte[] RawData { get; set; }
    }

    /// <summary>
    /// Reads all alternate data streams for a given file
    /// </summary>
    /// <param name="filePath">Full path to the file to analyze</param>
    /// <returns>Dictionary with stream names as keys and their content as values</returns>
    public Dictionary<string, StreamContent> ReadAlternateStreams(string filePath)
    {
        _streams.Clear();

        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException("The specified file was not found.", filePath);
        }

        var streamInfos = Directory.GetFiles(filePath + ":*");

        foreach (var streamPath in streamInfos)
        {
            try
            {
                string streamName = ExtractStreamName(streamPath);
                var content = ReadStreamContent(streamPath);

                if (!string.IsNullOrEmpty(streamName))
                {
                    _streams[streamName] = content;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error reading stream {streamPath}: {ex.Message}");
            }
        }

        return _streams;
    }

    /// <summary>
    /// Extracts the stream name from the full stream path
    /// </summary>
    private string ExtractStreamName(string streamPath)
    {
        int firstColon = streamPath.IndexOf(':', 2);
        if (firstColon == -1) return string.Empty;

        string streamPart = streamPath.Substring(firstColon + 1);
        return streamPart.Replace(":$DATA", "");
    }

    /// <summary>
    /// Reads and processes the content of a stream
    /// </summary>
    private StreamContent ReadStreamContent(string streamPath)
    {
        var content = new StreamContent();

        // Read the raw bytes
        byte[] rawBytes = File.ReadAllBytes(streamPath);
        content.RawData = rawBytes;

        if (rawBytes.Length == 0)
        {
            content.IsBinary = false;
            content.DisplayContent = string.Empty;
            content.HexView = string.Empty;
            return content;
        }

        // Try to convert to string using UTF8
        try
        {
            string stringContent = Encoding.UTF8.GetString(rawBytes);
            content.IsBinary = IsBinaryContent(rawBytes, stringContent);

            if (content.IsBinary)
            {
                content.DisplayContent = "<binary data>";
                // Generate hex view for binary data
                content.HexView = GenerateHexView(rawBytes);
            }
            else
            {
                // For text content, show first 1000 bytes as text
                content.DisplayContent = stringContent.Length > MAX_CONTENT_LENGTH 
                    ? stringContent.Substring(0, MAX_CONTENT_LENGTH) + "..."
                    : stringContent;

                // For text content longer than 50 chars, still generate hex view
                if (rawBytes.Length > MAX_CONTENT_LENGTH)
                {
                    content.HexView = GenerateHexView(rawBytes);
                }
            }
        }
        catch
        {
            content.IsBinary = true;
            content.DisplayContent = "<binary data>";
            content.HexView = GenerateHexView(rawBytes);
        }

        return content;
    }

    /// <summary>
    /// Generates a hex view of the data with ASCII representation
    /// </summary>
    /// <param name="bytes">Raw byte data</param>
    /// <returns>Formatted hex view string</returns>
    private string GenerateHexView(byte[] bytes)
    {
        var sb = new StringBuilder();

        // Limit to MAX_HEX_BYTES
        int bytesToShow = Math.Min(bytes.Length, MAX_HEX_BYTES);

        for (int i = 0; i < bytesToShow; i += BYTES_PER_LINE)
        {
            // Add offset
            sb.AppendFormat("{0:X8}: ", i);

            // Add hex values
            for (int j = 0; j < BYTES_PER_LINE; j++)
            {
                if (i + j < bytesToShow)
                {
                    sb.AppendFormat("{0:X2} ", bytes[i + j]);
                }
                else
                {
                    sb.Append("   "); // 3 spaces for alignment
                }
            }

            // Add separator
            sb.Append("| ");

            // Add ASCII representation
            for (int j = 0; j < BYTES_PER_LINE; j++)
            {
                if (i + j < bytesToShow)
                {
                    byte b = bytes[i + j];
                    // Show printable characters, replace others with dot
                    char c = (b >= 32 && b <= 126) ? (char)b : '.';
                    sb.Append(c);
                }
            }

            sb.AppendLine();
        }

        if (bytes.Length > MAX_HEX_BYTES)
        {
            sb.AppendLine("...");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Attempts to determine if content is binary
    /// </summary>
    private bool IsBinaryContent(byte[] bytes, string stringContent)
    {
        if (Array.IndexOf(bytes, (byte)0) != -1)
            return true;

        if (BinaryPattern.IsMatch(stringContent))
            return true;

        int nonPrintable = 0;
        foreach (char c in stringContent)
        {
            if (char.IsControl(c) && !char.IsWhiteSpace(c))
                nonPrintable++;
        }

        return (double)nonPrintable / stringContent.Length > 0.1;
    }
}

// Example usage:
public class Program
{
    public static void Main()
    {
        var reader = new AlternateStreamReader();
        try
        {
            var streams = reader.ReadAlternateStreams(@"C:\path\to\your\file.txt");
            foreach (var stream in streams)
            {
                Console.WriteLine($"Stream: {stream.Key}");
                Console.WriteLine($"Content: {stream.Value.DisplayContent}");

                if (!string.IsNullOrEmpty(stream.Value.HexView))
                {
                    Console.WriteLine("\nHex View:");
                    Console.WriteLine(stream.Value.HexView);
                }

                Console.WriteLine();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}