File: PackageExtraction\ZipArchiveExtensions.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Packaging\NuGet.Packaging.csproj (NuGet.Packaging)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using NuGet.Common;

namespace NuGet.Packaging
{
    /// <summary>
    /// Nupkg reading helper methods
    /// </summary>
    public static class ZipArchiveExtensions
    {
        public static ZipArchiveEntry LookupEntry(this ZipArchive zipArchive, string path)
        {
            var entry = zipArchive.Entries.FirstOrDefault(zipEntry => UnescapePath(zipEntry.FullName) == path);
            if (entry == null)
            {
                throw new FileNotFoundException(path);
            }

            return entry;
        }

        public static IEnumerable<string> GetFiles(this ZipArchive zipArchive)
        {
            return zipArchive.Entries.Select(e => UnescapePath(e.FullName));
        }

        private static string UnescapePath(string path)
        {
            if (path != null
#if NETCOREAPP
                && path.IndexOf('%', StringComparison.Ordinal) > -1)
#else
                && path.IndexOf('%') > -1)
#endif
            {
                return Uri.UnescapeDataString(path);
            }

            return path!;
        }

        public static Stream OpenFile(this ZipArchive zipArchive, string path)
        {
            var entry = LookupEntry(zipArchive, path);
            return entry.Open();
        }

        public static string SaveAsFile(this ZipArchiveEntry entry, string fileFullPath, ILogger logger)
        {
            using (var inputStream = entry.Open())
            {
                inputStream.CopyToFile(fileFullPath);
            }

            entry.UpdateFileTimeFromEntry(fileFullPath, logger);

            return fileFullPath;
        }

        public static void UpdateFileTimeFromEntry(this ZipArchiveEntry entry, string fileFullPath, ILogger logger)
        {
            Testable.Default.UpdateFileTimeFromEntry(entry, fileFullPath, logger);
        }

        internal static void UpdateFileTime(string fileFullPath, DateTime dateTime)
        {
            Testable.Default.UpdateFileTimeEntry(fileFullPath, dateTime);
        }

        internal class Testable
        {
            public static Testable Default { get; } = new Testable(EnvironmentVariableWrapper.Instance);

            internal Testable(IEnvironmentVariableReader environmentVariableReader)
            {
                _updateFileTimeFromEntryMaxRetries = 9;
                string? value = environmentVariableReader.GetEnvironmentVariable("NUGET_UPDATEFILETIME_MAXRETRIES");
                if (int.TryParse(value, out int maxRetries) && maxRetries > 0)
                {
                    _updateFileTimeFromEntryMaxRetries = maxRetries;
                }
            }

            private readonly int _updateFileTimeFromEntryMaxRetries;

            internal void UpdateFileTimeFromEntry(ZipArchiveEntry entry, string fileFullPath, ILogger logger)
            {
                if (entry == null) throw new ArgumentNullException(nameof(entry));
                if (fileFullPath == null) throw new ArgumentNullException(nameof(fileFullPath));
                logger ??= NullLogger.Instance;

                FileAttributes attr = File.GetAttributes(fileFullPath);

                if (!attr.HasFlag(FileAttributes.Directory) &&
                    entry.LastWriteTime.DateTime != DateTime.MinValue && // Ignore invalid times
                    entry.LastWriteTime.UtcDateTime <= DateTime.UtcNow) // Ignore future times
                {
                    try
                    {
                        DateTime dateTime = entry.LastWriteTime.Add(entry.LastWriteTime.Offset).UtcDateTime;
                        UpdateFileTimeEntry(fileFullPath, dateTime);
                    }
                    catch (ArgumentOutOfRangeException ex)
                    {
                        string message = string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.FailedFileTime,
                            fileFullPath, // {0}
                            ex.Message); // {1}

                        logger.LogVerbose(message);
                    }
                }
            }

            internal void UpdateFileTimeEntry(string fileFullPath, DateTime dateTime)
            {
                if (string.IsNullOrEmpty(fileFullPath)) throw new ArgumentNullException(nameof(fileFullPath));

                int retry = 0;
                bool successful = false;
                while (!successful)
                {
                    try
                    {
                        File.SetLastWriteTimeUtc(fileFullPath, dateTime);
                        successful = true;
                    }
                    catch (IOException) when (retry < _updateFileTimeFromEntryMaxRetries)
                    {
                        // Use exponentional backoff, to reduce CPU usage, allowing other threads to work, even if this
                        // isn't an async method and therefore requires the ThreadPool to spin up new threads.
                        Thread.Sleep(1 << retry);
                        retry++;
                    }
                }
            }
        }
    }
}