Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Difference in File/DirectoryNotFoundException behavior when both directory and file don't exist #21745

Closed
stephentoub opened this issue May 16, 2017 · 14 comments
Assignees
Labels
area-System.IO bug disabled-test The test is disabled in source code against the issue os-linux Linux OS (any supported distro) os-mac-os-x macOS aka OSX
Milestone

Comments

@stephentoub
Copy link
Member

This code on Windows:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        long x = new FileInfo(@"C:\users\stoub\doesntExist\doesntExist.txt").Length;
    }
}

throws a FileNotFoundException, whereas this code on Linux:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        long x = new FileInfo(@"/home/stoub/doesntExist/doesntExist.txt").Length;
    }
}

throws a DirectoryNotFoundException. (If the whole directory exists and just the file doesn't, then we throw a FileNotFoundException on Linux as expected.)

Difference reported by @polarapfel at https://github.com/dotnet/corefx/issues/1728#issuecomment-301935624

cc: @ianhays, @JeremyKuhne

@stephentoub
Copy link
Member Author

Hmmm. We have code in the Unix implementation especially to deal with what the comment claims is a difference:
https://github.com/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs#L270-L281
but that cited difference on Windows directly goes against the example I included in this issue. And if as an experiment I set:
https://github.com/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/FileSystemInfo.Unix.cs#L280
to always be false, no tests fail. I'm confused what situation this was meant to be handling.

@JeremyKuhne
Copy link
Member

Are you talking about the trimming of the final separator change to the original logic? You added the original check for this, didn't you?

@JeremyKuhne
Copy link
Member

JeremyKuhne commented May 17, 2017

Ok, so normally the Windows behavior matches what we're seeing on Unix. You get PATH_NOT_FOUND if the path to the left of the last separator doesn't exist. The kicker here is that on desktop we explicitly ignore the error value (_dataInitialised = File.FillAttributeInfo(FullPath, ref _data, false, false);).

The error always comes back as FileNotFound for FileInfo as a result. (Assuming it doesn't exist as a directory.) Ugh. If someone else gets a chance to take this before Thursday please assign it to yourself. Otherwise I'll take it then.

@JeremyKuhne
Copy link
Member

Note that I validated the Windows error behavior for both GetFileAttributes and FindFirstFile. They return PATH_NOT_FOUND in the same way.

@stephentoub
Copy link
Member Author

Are you talking about the trimming of the final separator change to the original logic?

I'm talking about the whole thing. I must have added this in response to something failing, but none if the existing tests are failing without it, and the behavior on Windows doesn't match the comment.

@JeremyKuhne JeremyKuhne self-assigned this May 18, 2017
@ianhays
Copy link
Contributor

ianhays commented May 18, 2017

I remember us discussing this. If I recall correctly, the issue was that Windows would provide more information in the error that we would use to determine whether to throw a DirectoryNotFound or a FileNotFound. On Unix the exceptions thrown from the interop call didn't include that info, so we did some manual parsing to throw a DirectoryNotFound or a FileNotFound depending on which piece was missing.

An example of this is way back when I added the parsing to the FileInfo CopyTo function. We had the same issue there and did the directory Exists check to throw a DNFE.

and the behavior on Windows doesn't match the comment.

Could this be because our interop exception catching/rethrowing logic has changed?

@JeremyKuhne
Copy link
Member

The problem is that in the case of the Info wrappers we ignored the actual error on NetFX. It is just obfuscated a bit as Refresh() explicitly asks to ignore errors on not found.

I'm working on this now, writing a full set of tests that match desktop.

@polarapfel
Copy link

Hey guys, it's great to see issues reported being picked up so quickly (and discussed openly here). Really enjoying this!!

Thanks!

@stephentoub
Copy link
Member Author

stephentoub commented May 18, 2017

The problem is that in the case of the Info wrappers we ignored the actual error on NetFX. It is just obfuscated a bit as Refresh() explicitly asks to ignore errors on not found.

Ah! So am I correct in understanding that this same logic is exercised via File/Directory (rather than FileInfo/DirectoryInfo), and my comment / fix was relevant to the former but causes problems for the latter? I guess that makes me feel a bit better that I wasn't totally crazy ;)

Hey guys, it's great to see issues reported being picked up so quickly (and discussed openly here). Really enjoying this!!

Excellent :)

@stephentoub
Copy link
Member Author

stephentoub commented May 18, 2017

And indeed changing the code from:

long x = new FileInfo(@"C:\users\stoub\doesntExist\doesntExist.txt").Length;

to:

File.GetAttributes(@"C:\users\stoub\doesntExist\doesntExist.txt");

results in a DirectoryNotFoundException instead of a FileNotFoundException. Gotta love inconsistency 😦

@JeremyKuhne
Copy link
Member

Gotta love inconsistency

Yeah- in this case it looks accidental as well (or at best evil). After the Refresh() call _dataInitialized will be 0 and _data.fileAttributes will be 0xFFFFFFFF.

    if (_dataInitialised == -1)
        Refresh();
    
    if (_dataInitialised != 0) // Refresh was unable to initialise the data
        __Error.WinIOError(_dataInitialised, DisplayPath);

    if ((_data.fileAttributes & Win32Native.FILE_ATTRIBUTE_DIRECTORY) != 0)
        __Error.WinIOError(Win32Native.ERROR_FILE_NOT_FOUND, DisplayPath);
    
    return ((long)_data.fileSizeHigh) << 32 | ((long)_data.fileSizeLow & 0xFFFFFFFFL);

@stephentoub
Copy link
Member Author

@JeremyKuhne, this can be closed now, right?

@daxian-dbw
Copy link
Contributor

Yeah- in this case it looks accidental as well (or at best evil). After the Refresh() call _dataInitialized will be 0 and _data.fileAttributes will be 0xFFFFFFFF.

Could dotnet/corefx#20456 be related to the discussion here? FileSystemInfo.Attributes gets set to -1 on Unix if the file is somehow deleted during an enumeration of all files in the parent directory. This happens with Microsoft.NetCore.App 2.0.0-preview2-25324-03 (built on 5/24). But the behavior is different with Microsoft.NetCore.App 2.0.0-preview1-002106-00, where a FileNotFoundException exception is thrown.

@JeremyKuhne
Copy link
Member

dotnet/coreclr#11757 fixed this in CoreCLR for master.
dotnet/coreclr#11807 ported the CoreCLR fix.

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 2.0.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 23, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.IO bug disabled-test The test is disabled in source code against the issue os-linux Linux OS (any supported distro) os-mac-os-x macOS aka OSX
Projects
None yet
Development

No branches or pull requests

6 participants