0

I'm having an issue where a file containing a period at the end of its name cannot be seen by FileInfo. I know that Windows prevents naming files this way, but this data came from a different operating system.

I am able to create a problem file in Windows by using command line:

echo "test" > "\\?\C:\Test\BadFileName."

and this is the code I was using to test the file:

DateTime origAccessDate;
DateTime OrigCreateDate;
long sizeBytes;

string path = @"\\?\C:\Test\BadFileName.";
FileInfo fi = new FileInfo(path);

try
{
     origAccessDate = fi.LastAccessTime;
     OrigCreateDate = fi.CreationTime;
     sizeBytes = fi.Length;
}
catch (Exception ex)
{
     MessageBox.Show(ex.Message);
}

The problem occurs when FileInfo is called on the path. The Exists property is false, even though you can copy/paste the path to confirm it is valid. The goal is not to rename the file in order to read, but to read it in place (as-is).

Koby Douek
  • 16,156
  • 19
  • 74
  • 103
complhex
  • 31
  • 2

3 Answers3

0

Since this is obviously an unsupported scenario, I doubt it can be done without using some low-level file access.

What you could try to do is drop FileInfo and go with File.Exists(path) and File.ReadAllBytes(path). These might be able to circumvent the issue.

File Access using SafeFileHandle

The following is untested

Creating an instance of UnmanagedFileLoader (Code below, taken from MSDN), allows you to create a SafeFileHandle object which can be passed to the FileStream constructor in the following way:

UnmanagedFileLoader ufl = new UnmanagedFileLoader(path);
FileStream fs = new FileStream(ufl.Handle, FileMode.Open);

Note: Remember to call ufl.Handle.Dispose().

This should give you a more, shall we say, direct access to the file, and so go around the enforcement of a valid filename that Windows has in place.

UnmanagedFileLoader Code

class UnmanagedFileLoader 
{
    public const short FILE_ATTRIBUTE_NORMAL = 0x80;
    public const short INVALID_HANDLE_VALUE = -1;
    public const uint GENERIC_READ = 0x80000000;
    public const uint GENERIC_WRITE = 0x40000000;
    public const uint CREATE_NEW = 1;
    public const uint CREATE_ALWAYS = 2;
    public const uint OPEN_EXISTING = 3;

    // Use interop to call the CreateFile function.
    // For more information about CreateFile,
    // see the unmanaged MSDN reference library.
    [DllImport("kernel32.dll", SetLastError = true, CharSet=CharSet.Unicode)]
    static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess,
      uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,
      uint dwFlagsAndAttributes, IntPtr hTemplateFile);

    private SafeFileHandle handleValue = null;

    public UnmanagedFileLoader(string Path)
    {
        Load(Path);
    }

    public void Load(string Path)
    {
        if (Path == null || Path.Length == 0)
        {
            throw new ArgumentNullException("Path");
        }

        // Try to open the file.
        handleValue = CreateFile(Path, GENERIC_WRITE, 0, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

        // If the handle is invalid,
        // get the last Win32 error 
        // and throw a Win32Exception.
        if (handleValue.IsInvalid)
        {
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        }
    }

    public SafeFileHandle Handle
    {
        get
        {
            // If the handle is valid,
            // return it.
            if (!handleValue.IsInvalid)
            {
                return handleValue;
            }
            else
            {
                return null;
            }
        }

    }
}

File Date Access using Windows API

The GetFileTimeSample class below, taken from www.pinvoke.net uses another Windows API call, specifically GetFileTime. This implementation is just an example, you sure will be able to adapt it to get only the date you need. In its current form it will output all three dates.

Usage:

DateTime fileDateCreated;
DateTime fileDateAccessed;
DateTime fileDateModified;
GetFileTimeSample.GetFileTimes(path, out fileDateCreated, out fileDateAccessed, out fileDateModified);

Since C# 7.0 it is possible to declare out variables directly in the function call like so:

GetFileTimeSample.GetFileTimes(path, out DateTime fileDateCreated, out DateTime fileDateAccessed, out DateTime fileDateModified);

GetFileTimeSample

public class GetFileTimeSample
{
    private const uint GENERIC_READ = 0x80000000;
    private const uint FILE_SHARE_READ = 0x1;
    private const uint FILE_ATTRIBUTE_NORMAL = 0x80;
    private const int INVALID_HANDLE_VALUE = -1;
    private const uint OPEN_EXISTING = 3;

    [StructLayout(LayoutKind.Sequential)]
    private struct FILETIME
    {
    public uint dwLowDateTime;
    public uint dwHighDateTime;
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CloseHandle(
    IntPtr hObject
    );

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    private static extern IntPtr CreateFile(
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    IntPtr SecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    IntPtr hTemplateFile
    );

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GetFileTime(
    IntPtr hFile,
    ref FILETIME lpCreationTime,
    ref FILETIME lpLastAccessTime,
    ref FILETIME lpLastWriteTime
    );

    public static void GetFileTimes(string FileName, out DateTime CreationTime, out DateTime LastAccessTime, out DateTime LastWriteTime)
    {
    CreationTime = DateTime.MinValue;
    LastAccessTime = DateTime.MinValue;
    LastWriteTime = DateTime.MinValue;
    IntPtr ptr = IntPtr.Zero;
    FILETIME ftCreationTime = new FILETIME();
    FILETIME ftLastAccessTime = new FILETIME();
    FILETIME ftLastWriteTime = new FILETIME();
    try
    {
        ptr = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);
        if (ptr.ToInt32() == INVALID_HANDLE_VALUE)
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        if (GetFileTime(ptr, ref ftCreationTime, ref ftLastAccessTime, ref ftLastWriteTime) != true)
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        CreationTime = DateTime.FromFileTimeUtc((((long)ftCreationTime.dwHighDateTime) << 32) | ((uint)ftCreationTime.dwLowDateTime));
        LastAccessTime = DateTime.FromFileTimeUtc((((long)ftLastAccessTime.dwHighDateTime) << 32) | ((uint)ftLastAccessTime.dwLowDateTime));
        LastWriteTime = DateTime.FromFileTimeUtc((((long)ftLastWriteTime.dwHighDateTime) << 32) | ((uint)ftLastWriteTime.dwLowDateTime));
    }
    catch (Exception e)
    {
        throw (e);
    }
    finally
    {
        if (ptr !=IntPtr.Zero && ptr.ToInt32() != INVALID_HANDLE_VALUE) CloseHandle(ptr);
    }
    }
}
r41n
  • 908
  • 7
  • 18
  • Very nice! I'm able to get the size of the file using your method, but unsure how I can obtain the date information. – complhex Mar 30 '17 at 13:19
  • @complhex, which of the two methods? If your are talking about the `File` Class, there are `File.GetLastAccessTime` and `File.GetLastWriteTime`. – r41n Mar 31 '17 at 06:02
0

Here's a way to do file ops via the commandline. Not the most elegant solution, but hopefully a useful reference.

using System;
using System.IO;
using System.Diagnostics;

namespace StackOverflow_FileNameShenanigans
{
    class Program
    {
        static void Main(string[] args)
        {
            string contents;

            DateTime origAccessDate;
            DateTime origCreateDate;
            long sizeBytes;

            string path = @"\\?\C:\Test\BadFileName.";

            try
            {
                contents = CommandLineFileOps.ReadAllText(path);
                origAccessDate = CommandLineFileOps.LastAccessTime(path);
                origCreateDate = CommandLineFileOps.CreationTime(path);
                sizeBytes = CommandLineFileOps.Length(path);

                Console.WriteLine($"Contents: {contents}");
                Console.WriteLine($"OrigAccessDate: {origAccessDate}");
                Console.WriteLine($"OrigCreateDate: {origCreateDate}");
                Console.WriteLine($"SizeBytes: {sizeBytes}");
                Console.ReadKey();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                Console.ReadKey();
            }
        }

    }

    public static class CommandLineFileOps
    {
        public static string ReadAllText(string path)
        {
            string contents;
            RunOnCommandLine($"type {path}", out contents);

            contents = contents.Substring(0, contents.Length - 3);
            return contents;
        }

        public static DateTime CreationTime(string path)
        {
            string output;
            RunOnCommandLine($"dir /T:C {path}", out output);

            string dateLine = output.Split('\n')[5];
            string dateStr = dateLine.Replace("                 ", "\n").Split('\n')[0];
            return DateTime.Parse(dateStr);
        }

        public static DateTime LastAccessTime(string path)
        {
            string output;
            RunOnCommandLine($"dir /T:A {path}", out output);

            string dateLine = output.Split('\n')[5];
            string dateStr = dateLine.Replace("                 ", "\n").Split('\n')[0];
            return DateTime.Parse(dateStr);
        }

        public static long Length(string path)
        {
            string output;
            RunOnCommandLine($"dir {path}", out output);

            string lengthLine = output.Split('\n')[6];
            string lengthStr = lengthLine.Replace("              ", "\n").Split('\n')[2].Split(' ')[0];
            return long.Parse(lengthStr);
        }

        private static int RunOnCommandLine(string line)
        {
            Process cmd = new Process();
            cmd.StartInfo.FileName = "cmd.exe";
            cmd.StartInfo.RedirectStandardInput = true;
            cmd.StartInfo.RedirectStandardOutput = true;
            cmd.StartInfo.CreateNoWindow = true;
            cmd.StartInfo.UseShellExecute = false;
            cmd.Start();

            cmd.StandardInput.WriteLine(line);
            cmd.StandardInput.Flush();
            cmd.StandardInput.Close();
            cmd.WaitForExit();

            int exitCode = cmd.ExitCode;
            return exitCode;
        }

        private static int RunOnCommandLine(string line, out string output)
        {
            string tempPath = Path.GetTempFileName();
            int exitCode = RunOnCommandLine($"{line} > {tempPath}");
            output = File.ReadAllText(tempPath);
            File.Delete(tempPath);

            return exitCode;
        }
    }
}
C. McCoy IV
  • 887
  • 7
  • 14
  • I had errors on RunOnCommandLine calls (the $). Switched them to @ and got index out of bounds errors. – complhex Mar 30 '17 at 13:21
  • You may be compiling a lower version of .NET. "$" is an interpolation operator. It just allows you to write stuff like: "Contents: " + contents as $"Contents: {contents}". Also, all "@" does is specify that you're using a literal string. It's very useful for strings where you'd otherwise have to escape many characters, like file paths. – C. McCoy IV Mar 30 '17 at 17:05
  • I was able to use command line to solve the problem thanks to your help. I'll post my solution below. The only thing that I wasn't able to do is capture the time with second precision. – complhex Mar 30 '17 at 21:11
  • Very far from being elegant indeed, this definitively is not an acceptable approach while working with a programming language like C#. – r41n Mar 31 '17 at 12:50
0

Method calls:

        string path = @"C:\Test\BadFileName.";

        DateTime createDate = cmdGetCreateDate(path);
        DateTime accessDate = cmdGetAccessDate(path);
        long bytes = cmdGetSizeBytes(path);

Methods:

    private DateTime cmdGetCreateDate(string path)
    {
        DateTime createDate = new DateTime();
        int lastSlash = path.LastIndexOf(Convert.ToChar("\\"));
        string file = path.Substring(lastSlash + 1);
        string folder = path.Substring(0, lastSlash);

        string cmdexe = @"C:\Windows\System32\cmd.exe";
        string args = @"/c dir /T:C /A:-D """ + folder + "\"";

        Process procCreateDate = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = cmdexe,
                Arguments = args,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };

        procCreateDate.Start();
        string output = procCreateDate.StandardOutput.ReadToEnd();

        if (!output.Contains(file))
        {
            return createDate; //File not found
        }

        string p = @"\b\d{2}/\d{2}/\d{4}\b\s+\d{2}:\d{2} ..";
        Regex rx = new Regex(p);
        Match m = rx.Match(output);

        if (m.Success)
        {
            DateTime.TryParse(m.Value, out createDate);
        }

        return createDate;
    }

private DateTime cmdGetAccessDate(string path)
    {
        DateTime accessDate = new DateTime();
        int lastSlash = path.LastIndexOf(Convert.ToChar("\\"));
        string file = path.Substring(lastSlash + 1);
        string folder = path.Substring(0, lastSlash);

        string cmdexe = @"C:\Windows\System32\cmd.exe";
        string args = @"/c dir /T:A /A:-D """ + folder + "\"";

        Process procCreateDate = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = cmdexe,
                Arguments = args,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };

        procCreateDate.Start();
        string output = procCreateDate.StandardOutput.ReadToEnd();

        if (!output.Contains(file))
        {
            return accessDate; //File not found
        }

        string p = @"\b\d{2}/\d{2}/\d{4}\b\s+\d{2}:\d{2} ..";
        Regex rx = new Regex(p);
        Match m = rx.Match(output);

        if (m.Success)
        {
            DateTime.TryParse(m.Value, out accessDate);
        }

        return accessDate;
    }

    private long cmdGetSizeBytes(string path)
    {
        long bytes = -1;
        int lastSlash = path.LastIndexOf(Convert.ToChar("\\"));
        string file = path.Substring(lastSlash + 1);
        string folder = path.Substring(0, lastSlash);

        string cmdexe = @"C:\Windows\System32\cmd.exe";
        string args = @"/c dir /A:-D """ + folder + "\"";


        Process procCreateDate = new Process
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = cmdexe,
                Arguments = args,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };

        procCreateDate.Start();
        string output = procCreateDate.StandardOutput.ReadToEnd();

        if (!output.Contains(file))
        {
            return bytes; //File not found
        }

        string p = @"\d+ " + file;
        Regex rx = new Regex(p);
        Match m = rx.Match(output);

        if (m.Success)
        {
            string[] splitVal = m.Value.Split(Convert.ToChar(" "));
            bytes = Convert.ToInt64(splitVal[0]);
        }

        return bytes;
    }
complhex
  • 31
  • 2