4

I am trying to P/Invoke SetFileTime, but I can't seem to get it to work. It always returns an error code of 5 for me (access denied), and I am not sure why. Here is the code I am using:

void Main()
{
    var pointer = CreateFile(@"C:\Users\User\Desktop\New folder\New Text Document.txt", FileAccess.ReadWrite, FileShare.None, IntPtr.Zero, FileMode.Open, FileAttributes.Normal, IntPtr.Zero);
    Console.WriteLine(pointer);
    var now = DateTime.Now.ToFileTime();
    long lpCreationTime = now;
    long lpLastAccessTime = now;
    long lpLastWriteTime = now;
    if (!SetFileTime(pointer, ref lpCreationTime, ref lpLastAccessTime, ref lpLastWriteTime))
        Console.WriteLine(GetLastError());
    CloseHandle(pointer);
}

[DllImport("kernel32.dll")]
static extern UInt32 GetLastError();

[DllImport("kernel32.dll", SetLastError = true)]
static extern Boolean SetFileTime(IntPtr hFile, ref long lpCreationTime, ref long lpLastAccessTime, ref long lpLastWriteTime);

[DllImport("kernel32.dll", SetLastError = true)]
static extern Boolean CloseHandle(IntPtr hObject);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr CreateFile(String fileName, [MarshalAs(UnmanagedType.U4)] FileAccess fileAccess, [MarshalAs(UnmanagedType.U4)] FileShare fileShare, IntPtr securityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, [MarshalAs(UnmanagedType.U4)] FileAttributes flags, IntPtr template);

My pointer is valid (2376) but I see that SetFileTime has failed and has returned an error code of 5 (access denied). Now, I have ensured I am running as administrator and that my account has permissions to that path, but still, no cigar. Anyone have any ideas why this is happening?

Update

Marshal.GetLastWin32Error() also returns 5 after the call to SetFileTime. Also, my need for making this call work is so that I can SetFileTime on long paths in Windows, which CreateFile supports, but the current file libraries of .NET do not support long paths in Windows.

Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • @Alexandru have you thought to do a google search on the Method that you are trying to `P/Invoke SetFileTime` – MethodMan May 12 '15 at 01:59
  • @MethodMan Yeah, I wouldn't be asking if I hadn't. I have read its API documentation entirely. – Alexandru May 12 '15 at 02:00
  • there are more resources / examples out there beyond the `API` – MethodMan May 12 '15 at 02:02
  • Is there any reason for ignoring the .net framework methods to do this? – David Heffernan May 12 '15 at 02:07
  • @DavidHeffernan Is there a .NET Framework method to do this? I can use Reflector to see the source. – Alexandru May 12 '15 at 02:08
  • @DavidHeffernan Only reason to do this is CreateFile supports long path names, so I need something that can make use of the handle returned from a long path. I don't think you can get that out of the box with .NET. – Alexandru May 12 '15 at 02:09
  • Have you tried this: https://msdn.microsoft.com/en-us/library/system.io.filesysteminfo.creationtime(v=vs.110).aspx and it didn't work? How long is your path? – Ron Beyer May 12 '15 at 02:17
  • @Ron Paths longer than 260 need to be escaped and some .net methods reject such escaped paths. – David Heffernan May 12 '15 at 02:28
  • @RonBeyer Windows Explorer does not support long paths. You'll need to use some sort of extended terminal application to create them, such as Far Manager (http://www.farmanager.com/). The Windows API has functions that permit an extended-length path for a maximum total path length of 32,767 characters. Most .NET methods don't support paths over 260 characters. These paths need to be prefixed with \\?\ for long local paths or \\?\UNC\ for long UNC paths. FindFirstFile (https://msdn.microsoft.com/en-us/library/windows/desktop/aa364418%28v=vs.85%29.aspx) is an example of such a function. – Alexandru May 17 '15 at 19:05

2 Answers2

6

From the documentation of SetFileTime:

A handle to the file or directory. The handle must have been created using the CreateFile function with the FILE_WRITE_ATTRIBUTES access right.

Your code doesn't manage to do that. The .net FileAccess enumeration is not compatible with Win32 access flags. You'll need to define an enum specifically for use with CreateFile. Likewise your use of FileShare and FileMode are not correct.

This p/invoke declaration should suffice: http://www.pinvoke.net/default.aspx/kernel32.createfile


As Alexei said, don't call GetLastError because you may be picking up an error code for a framework call rather than the true error. Use SetLastError = true and Marshal.GetLastWin32Error().

You also fail to check for errors in the return value if CreateFile.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • so why here http://www.pinvoke.net/default.aspx/kernel32.createfile they suggest .net FileAccess in the top section ? – IronHide May 09 '19 at 11:52
  • @IronHide sorry, I don't understand your question – David Heffernan May 09 '19 at 11:53
  • I was refering to this code under that link `[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr CreateFile( [MarshalAs(UnmanagedType.LPTStr)] string filename, [MarshalAs(UnmanagedType.U4)] FileAccess access, ` here is .net FileAccess – IronHide May 10 '19 at 08:39
  • @IronHide OK, that looks wrong to me. The ordinals of `FileAccess` don't match `GENERIC_READ` and `GENERIC_WRITE`. And the more esoteric flags are not present at all. – David Heffernan May 10 '19 at 08:47
1

Thanks to all the help, I was able to achieve my goals using the following (FileAccess.ReadWrite translates to 0x3 while FILE_WRITE_ATTRIBUTES is 0x100):

[DllImport("kernel32.dll", SetLastError = true)]
static extern Boolean SetFileTime(SafeFileHandle hFile, ref long lpCreationTime, ref long lpLastAccessTime, ref long lpLastWriteTime);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFile(String fileName, uint fileAccess, [MarshalAs(UnmanagedType.U4)] FileShare fileShare, IntPtr securityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, [MarshalAs(UnmanagedType.U4)] FileAttributes flags, IntPtr template);

static void Main(string[] args)
{
    var handle = CreateFile(@"C:\Users\User\Desktop\FileTimeTest.txt", (uint)(0x3 | 0x100), FileShare.None, IntPtr.Zero, FileMode.Create, FileAttributes.Normal, IntPtr.Zero);
    if (!handle.IsInvalid)
    {
        using (var stream = new FileStream(handle, FileAccess.ReadWrite))
        using (var writer = new StreamWriter(stream))
        {
            writer.WriteLine("Hello, world.");
            var now = DateTime.MaxValue.ToFileTime();
            if (!SetFileTime(handle, ref now, ref now, ref now))
                Console.WriteLine(Marshal.GetLastWin32Error());
        }
    }
}
Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • I don't know why you continue to use `FileShare` and `FileMode`. They are just as wrong as `FileAccess`. – David Heffernan May 12 '15 at 12:33
  • `FileShare` or `FileMode` are not wrong. They works as expected with `CreateFile`. They just don't encompass the entire set of possible options (which are overkill for what I need). – Alexandru May 12 '15 at 13:10
  • I don't believe that they are correct. You've checked the numeric values? – David Heffernan May 12 '15 at 13:18
  • @DavidHeffernan I haven't, but I know that they work for their intended purposes through my own experimentation (and I've done a lot around CreateFile). If you don't believe me, you can see its not just me that uses them, as there are various other method signatures which make use of them when calling CreateFile (http://www.pinvoke.net/default.aspx/kernel32.createfile). I don't think that so many people would P/Invoke like this if it did not work for them. Most importantly however, remember that these are bit-wise flags, where the important thing is for the flags to contain the bits they need. – Alexandru May 12 '15 at 14:08
  • 1
    No, the pinvoke.net signature, which I referred you to in my answer, defines enums with the appropriate bit flags. It so happens that `FileShare` matches. Presumably that was done on purpose and I doubt that the values will change. `FileMode` has an extra value `FileMode.Append` which `CreateFile` does not understand. `FileMode` is a true enumeration, not bitflags. In my opinion you should stop abusing these types and do it the way pinvoke.net shows. – David Heffernan May 12 '15 at 14:15
  • @DavidHeffernan That's true. I never tried append, I guess I just never found a need for it. Either way, I don't call this directly, so I can expose my own accessors and do like .NET to seek to the end of the stream if this is specified. I get your point and its good to be aware of these things. – Alexandru May 12 '15 at 14:27
  • Just used dotPeek and, technically .NET doesn't have a proper CreateFile definition either since it takes `int` parameters where it should probably take `uint` for DWORD consistency...I wonder if there's any flags you can make it specify within any of its file libraries that make use of this method which cause these flags to go above 0x7FFFFFFF but below 0xFFFFFFFF. It sure does a lot of work behind the scenes, that's for sure. – Alexandru May 12 '15 at 14:46
  • I don't think that `int` vs `uint` is a real issue here. The same bit pattern ends up in `CreateFile`. – David Heffernan May 12 '15 at 15:01
  • @DavidHeffernan But then if any flags go above 0x7FFFFFFF but below 0xFFFFFFFF, then there will be lost bits when the `uint` is truncated to an `int`. Probably flags that are both used by `CreateFile` and that large don't exist, but it would be nice to see them support the possibility of expansion in the future or at least some more consistency... – Alexandru May 12 '15 at 15:09
  • It depends on the exact code, but it's not a given that there will be truncation. – David Heffernan May 12 '15 at 15:20
  • @DavidHeffernan Thinking about it some more, it shouldn't matter if you use `int` for a `DWORD` P/Invoke. My original beef was that `uint` does not have a leading bit for sign while `int` does, which means that you may pass a negative flag value in some cases, but I'm willing to bet that the marshalling layer takes care of that and converts all of the bits of the `int` data type to a `DWORD` type, so it should be fine. The sign bit should be included as a leading `DWORD` bit and add to its unsigned value. – Alexandru May 22 '15 at 12:38
  • That's it. A reinterp cast. – David Heffernan May 22 '15 at 12:43
  • @DavidHeffernan On second thought, I'm not sure anymore, as `unchecked { Console.WriteLine((uint)(-Int32.MaxValue)); }` prints `2147483649`. Are you sure it does a reinterpret cast? – Alexandru May 22 '15 at 12:44
  • @DavidHeffernan So an `int` value of `-Int32.MaxValue = -2147483649` should take on a `DWORD` value of `UInt32.MaxValue = 4294967295` when you P/Invoke then? – Alexandru May 22 '15 at 12:50
  • You are missing some -1s in there – David Heffernan May 22 '15 at 12:59
  • @DavidHeffernan What do you mean? – Alexandru May 22 '15 at 13:01
  • That was a misthink. Perhaps what you are missing is that as the bits cross the interop boundary, they are reinterpreted. The managed side has `int` and the unmanaged side treats those `int` bits as `DWORD`. That's the reinterpretation. – David Heffernan May 22 '15 at 13:04
  • @DavidHeffernan That's what I said. `-Int32.MaxValue = -0x7FFFFFFF = -2147483649 = 0b1111111111111111111111111111`, where the leading bit represents the sign, so this should become a `DWORD` of the same binary value, so `0b1111111111111111111111111111 = 0xFFFFFFFF = UInt32.MaxValue = 4294967295`. The binary value is the same, the only difference is that the int datatype interprets the leading bit as a negative value. – Alexandru May 22 '15 at 15:54