File: FileState.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// CopyFile delegate
    ///
    /// returns  Success = true, Failure = false; Retry = null
    /// </summary>
    /// <param name="source">Source file</param>
    /// <param name="destination">Destination file</param>
    internal delegate bool? CopyFileWithState(FileState source, FileState destination);
 
    /// <summary>
    /// Short-term cache saves the result of IO operations on a filename. Should only be
    /// used in cases where it is know there will be no exogenous changes to the filesystem
    /// for this file.
    /// </summary>
    /// <remarks>
    /// Uses PInvoke rather than FileInfo because the latter does all kinds of expensive checks.
    ///
    /// Deficiency: some of the properties eat some or all exceptions. If they are called first, they will
    /// trigger the population and eat. Subsequent calls will then not throw, but instead eg return zero.
    /// This could be fixed by storing the exception from the population, and throwing no matter who does
    /// the population and whether it's been done before.
    /// </remarks>
    internal class FileState
    {
        private class FileDirInfo
        {
            /// <summary>
            /// The name of the file.
            /// </summary>
            private readonly string _filename;
 
            /// <summary>
            /// Set to true if file or directory exists
            /// </summary>
            public readonly bool Exists;
 
            /// <summary>
            /// Set to true if the path referred to a directory.
            /// </summary>
            public readonly bool IsDirectory;
 
            /// <summary>
            /// File length
            /// </summary>
            public readonly long Length;
 
            /// <summary>
            /// Last time the file was updated
            /// </summary>
            public readonly DateTime LastWriteTimeUtc;
 
            /// <summary>
            /// True if the file is readonly
            /// </summary>
            public readonly bool IsReadOnly;
 
            /// <summary>
            /// Exception thrown on creation
            /// </summary>
            private readonly Exception _exceptionThrown;
 
            /// <summary>
            /// Constructor gets the data for the filename.
            /// On Win32 it uses native means. Otherwise,
            /// uses standard .NET FileInfo/DirInfo
            /// </summary>
            public FileDirInfo(string filename)
            {
                Exists = false;
 
                // If file/directory does not exist, return 12 midnight 1/1/1601.
                LastWriteTimeUtc = new DateTime(1601, 1, 1);
 
                _filename = FileUtilities.AttemptToShortenPath(filename); // This is no-op unless the path actually is too long
 
                int oldMode = 0;
 
                if (NativeMethodsShared.IsWindows)
                {
                    // THIS COPIED FROM THE BCL:
                    //
                    // For floppy drives, normally the OS will pop up a dialog saying
                    // there is no disk in drive A:, please insert one.  We don't want that.
                    // SetErrorMode will let us disable this, but we should set the error
                    // mode back, since this may have wide-ranging effects.
                    NativeMethodsShared.SetThreadErrorMode(1 /* ErrorModes.SEM_FAILCRITICALERRORS */, out oldMode);
                }
 
                try
                {
                    if (NativeMethodsShared.IsWindows)
                    {
                        var data = new NativeMethodsShared.WIN32_FILE_ATTRIBUTE_DATA();
                        bool success = NativeMethodsShared.GetFileAttributesEx(_filename, 0, ref data);
 
                        if (!success)
                        {
                            int error = Marshal.GetLastWin32Error();
 
                            // File not found is the most common case, for example we're copying
                            // somewhere without a file yet. Don't do something like FileInfo.Exists to
                            // get a nice error, or we're doing IO again! Don't even format our own string:
                            // that turns out to be unacceptably expensive here as well. Set a flag for this particular case.
                            //
                            // Also, when not under debugger (!) it will give error == 3 for path too long. Make that consistently throw instead.
                            if ((error == 2 /* ERROR_FILE_NOT_FOUND */|| error == 3 /* ERROR_PATH_NOT_FOUND */)
                                && _filename.Length <= NativeMethodsShared.MaxPath)
                            {
                                Exists = false;
                                return;
                            }
 
                            // Throw nice message as far as we can. At this point IO is OK.
                            Length = new FileInfo(_filename).Length;
 
                            // Otherwise this will give at least something
                            NativeMethodsShared.ThrowExceptionForErrorCode(error);
                            ErrorUtilities.ThrowInternalErrorUnreachable();
                        }
 
                        Exists = true;
                        IsDirectory = (data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_DIRECTORY) != 0;
                        IsReadOnly = !IsDirectory
                                      && (data.fileAttributes & NativeMethodsShared.FILE_ATTRIBUTE_READONLY) != 0;
                        LastWriteTimeUtc =
                            DateTime.FromFileTimeUtc(((long)data.ftLastWriteTimeHigh << 0x20) | data.ftLastWriteTimeLow);
                        Length = IsDirectory ? 0 : (((long)data.fileSizeHigh << 0x20) | data.fileSizeLow);
                    }
                    else
                    {
                        var fileInfo = new FileInfo(_filename);
 
                        if (fileInfo.Exists)
                        {
                            // Use FileInfo to get readonly and last write date
                            Exists = true;
                            IsReadOnly = fileInfo.IsReadOnly;
                            LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
                            Length = fileInfo.Length;
                        }
                        else
                        {
                            var directoryInfo = new DirectoryInfo(_filename);
 
                            if (directoryInfo.Exists)
                            {
                                // Use DirectoryInfo to get the last write date
                                Exists = true;
                                IsDirectory = true;
                                IsReadOnly = false;
                                LastWriteTimeUtc = directoryInfo.LastWriteTimeUtc;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    // Save the exception thrown and assume the file does not exist
                    _exceptionThrown = ex;
                    Exists = false;
                }
                finally
                {
                    // Reset the error mode on Windows
                    if (NativeMethodsShared.IsWindows)
                    {
                        NativeMethodsShared.SetThreadErrorMode(oldMode, out _);
                    }
                }
            }
 
            /// <summary>
            /// Throw exception as if the FileInfo did it. We
            /// know that getting the length of a file would
            /// throw exception if there are IO problems
            /// </summary>
            public void ThrowFileInfoException(bool doThrow)
            {
                if (doThrow)
                {
                    // Provoke exception
                    var length = (new FileInfo(_filename)).Length;
                }
            }
 
            /// <summary>
            /// Throw non-IO-related exception if occurred during creation.
            /// Return true if exception did occur, but was IO-related
            /// </summary>
            public bool ThrowNonIoExceptionIfPending()
            {
                if (_exceptionThrown != null)
                {
                    if (!ExceptionHandling.IsIoRelatedException(_exceptionThrown))
                    {
                        throw _exceptionThrown;
                    }
 
                    return true;
                }
 
                return false;
            }
 
            /// <summary>
            /// Throw any exception collected during construction
            /// </summary>
            public void ThrowException()
            {
                if (_exceptionThrown != null)
                {
                    throw _exceptionThrown;
                }
            }
        }
 
        /// <summary>
        /// The name of the file.
        /// </summary>
        private readonly string _filename;
 
        /// <summary>
        /// Holds the full path equivalent of _filename
        /// </summary>
        public string FileNameFullPath;
 
        /// <summary>
        /// Actual file or directory information
        /// </summary>
        private Lazy<FileDirInfo> _data;
 
        /// <summary>
        /// Constructor.
        /// Only stores file name: does not grab the file state until first request.
        /// </summary>
        internal FileState(string filename)
        {
            ErrorUtilities.VerifyThrowArgumentLength(filename);
            _filename = filename;
            _data = new Lazy<FileDirInfo>(() => new FileDirInfo(_filename));
        }
 
        /// <summary>
        /// Whether the file is readonly.
        /// Returns false for directories.
        /// Throws if file does not exist.
        /// </summary>
        internal bool IsReadOnly => !DirectoryExists && _data.Value.IsReadOnly;
 
        /// <summary>
        /// Whether the file exists.
        /// Returns false if it is a directory, even if it exists.
        /// Returns false instead of IO related exceptions.
        /// </summary>
        internal bool FileExists => !_data.Value.ThrowNonIoExceptionIfPending() && (_data.Value.Exists && !_data.Value.IsDirectory);
 
        /// <summary>
        /// Whether the directory exists.
        /// Returns false for files.
        /// Returns false instead of IO related exceptions.
        /// </summary>
        internal bool DirectoryExists => !_data.Value.ThrowNonIoExceptionIfPending() && (_data.Value.Exists && _data.Value.IsDirectory);
 
        /// <summary>
        /// Last time the file was written.
        /// Works for directories.
        /// </summary>
        internal DateTime LastWriteTime => LastWriteTimeUtcFast.ToLocalTime();
 
        /// <summary>
        /// Last time the file was written, in UTC. Avoids translation for daylight savings, time zone etc which isn't needed for just comparisons.
        /// If file does not exist, returns 12 midnight 1/1/1601.
        /// Works for directories.
        /// </summary>
        internal DateTime LastWriteTimeUtcFast
        {
            get
            {
                _data.Value.ThrowException();
                return _data.Value.Exists ? _data.Value.LastWriteTimeUtc : new DateTime(1601, 1, 1);
            }
        }
 
        /// <summary>
        /// Length of the file in bytes.
        /// Throws if it is a directory.
        /// Throws if it does not exist.
        /// </summary>
        internal long Length
        {
            get
            {
                _data.Value.ThrowException();
                _data.Value.ThrowFileInfoException(!_data.Value.Exists || _data.Value.IsDirectory);
                return _data.Value.Length;
            }
        }
 
        /// <summary>
        /// Name of the file as it was passed in.
        /// Not normalized.
        /// </summary>
        internal string Name => _filename;
 
        /// <summary>
        /// Whether this is a directory.
        /// Throws if it does not exist.
        /// </summary>
        internal bool IsDirectory
        {
            get
            {
                _data.Value.ThrowException();
                _data.Value.ThrowFileInfoException(!_data.Value.Exists);
                return _data.Value.IsDirectory;
            }
        }
 
        /// <summary>
        /// Use in case the state is known to have changed exogenously.
        /// </summary>
        internal void Reset()
        {
            _data = new Lazy<FileDirInfo>(() => new FileDirInfo(_filename));
        }
    }
}