Michał Masny
Michał Masny

Reputation: 229

Constructing a FileStream from a handle produced by CreateFile gives an empty stream

I'm attempting to write a simple program that would take a CSV consisting of two columns, and given a keyword from the first column, return the corresponding value from the second one. The problem is that I need the CSV to be in an alternate data stream to make the program as portable as possible (I want to make it so that when the user drops a CSV file on the executable, the CSV is overwritten). That is why I'm trying to use WinAPI's CreateFile function -- .NET doesn't support alternate data streams. Unfortunately, I'm failing miserably.

In the current state, the program is supposed to read a file named test.csv. I want to do CreateFile on it, convert the intPtr handle to a SafeFileHandle and then pass the SafeFileHandle to a FileStream constructor. The best I've been able to achieve is an empty stream though. It doesn't seem to me that the program is actually getting the right handle. When I try "CREATE_ALWAYS" or "CREATE_NEW" instead of "OPEN_ALWAYS", I'm getting an "Invalid Handle" error, no matter what I do with the rest of the parameters. With "OPEN_ALWAYS", when I check the value of "stream.Name", I'm getting "Unknown".

Here is the code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Searcher
{
    public partial class Searcher : Form
    {
        public static List<string> keywords;
        public static List<string> values;

        public Searcher()
        {
            InitializeComponent();

            //byte[] path = Encoding.UTF8.GetBytes("C:\\Users\\as\\Documents\\test.csv");
            SafeFileHandle safeADSHandle = NativeMethods.CreateFileW("test.csv",
            NativeConstants.GENERIC_READ,
            NativeConstants.FILE_SHARE_READ,
            IntPtr.Zero,
            NativeConstants.OPEN_ALWAYS,
            0,
            IntPtr.Zero);
            //var safeADSHandle = new SafeFileHandle(handle, true);
            if (safeADSHandle.IsInvalid)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
            var stream = new FileStream(safeADSHandle, FileAccess.Read);
            MessageBox.Show(stream.Name);
            var reader = new StreamReader(stream);
            Searcher.keywords = new List<string>();
            Searcher.values = new List<string>();
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                var values = line.Split(',');
                Searcher.keywords.Add(values[0]);
                Searcher.values.Add(values[1]);
            }
            cbKeyword.DataSource = Searcher.keywords;
            cbKeyword.AutoCompleteSource = AutoCompleteSource.ListItems;
        }

        private void btnSearch_Click(object sender, EventArgs e)
        {
            tbResult.Text = Searcher.values[cbKeyword.SelectedIndex];
        }
    }
}

public partial class NativeMethods
{
        [DllImportAttribute("kernel32.dll", SetLastError = true, EntryPoint = "CreateFile")]
        public static extern SafeFileHandle CreateFileW(
            [InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string lpFileName,
            uint dwDesiredAccess,
            uint dwShareMode,
            [InAttribute()] System.IntPtr lpSecurityAttributes,
            uint dwCreationDisposition,
            uint dwFlagsAndAttributes,
            [InAttribute()] System.IntPtr hTemplateFile
        );
}


public partial class NativeConstants
{

        /// GENERIC_WRITE -> (0x40000000L)
        public const int GENERIC_WRITE = 1073741824;

        public const uint GENERIC_READ = 2147483648;

        /// FILE_SHARE_DELETE -> 0x00000004
        public const int FILE_SHARE_DELETE = 4;

        /// FILE_SHARE_WRITE -> 0x00000002
        public const int FILE_SHARE_WRITE = 2;

        /// FILE_SHARE_READ -> 0x00000001
        public const int FILE_SHARE_READ = 1;

        /// OPEN_ALWAYS -> 4
        public const int OPEN_ALWAYS = 4;
        public const int CREATE_NEW = 1;
}

Edit I've changed the code above slightly to reflect the changes after the comments. Now I'm not converting from IntPtr to SafeFileHandle. One thing to mention perhaps is that I before this change I tried reading the value of handle.ToString() to see if it was changing and it was -- it's a random number.

Upvotes: 1

Views: 2410

Answers (1)

Sam
Sam

Reputation: 3480

The reason this is not working is because you have specified an entry point without a character set. When you don't specify a character set in DllImport, the default is CharSet.Ansi, which means platform invoke will search for the function as follows:

  • First for an unmangled exported function, i.e. the name CreateFile from the entry point that you specified (which does not exist in kernel32.dll)
  • Next if the unmangled name was not found, it will search for the mangled name based on the charset, so it will search for CreateFileA

So it will find the CreateFileA exported function, which assumes any strings passed in to it are in 1-byte character ANSI format. However, you are marshalling the string as a wide string. Note that the wide-character version of the function is called CreateFileW (this distinction between ANSI and wide character versions of functions is common in the Windows API).

To fix this you just need to make sure that the marshalling of your parameters matches up with what the function you are importing expects. So you could remove the EntryPoint field, in which case it will use the C# method name to find the exported function instead (so it will find CreateFileW).

However to make it even clearer, I would write your platform invoke code as follows:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
    [MarshalAs(UnmanagedType.LPTStr)] string filename,
    [MarshalAs(UnmanagedType.U4)] FileAccess access,
    [MarshalAs(UnmanagedType.U4)] FileShare share,
    IntPtr securityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
    IntPtr templateFile);

I took this from the pinvoke.net website, which should be your go-to for writing pinvoke code rather than trying to bash it out yourself (especially if you aren't familiar with the Windows API or marshalling :).

The reason there was no error is probably because CreateFile was creating the file since it wouldn't have been able to find it.

The file name will appear as "[Unknown]" though. I suspect this is because the code to get a file name from a handle is non-trivial.

Upvotes: 4

Related Questions