diff --git a/app/Pog/lib_compiled/Pog/src/Commands/ContainerCommands/InstallFromUrlCommand.cs b/app/Pog/lib_compiled/Pog/src/Commands/ContainerCommands/InstallFromUrlCommand.cs index 6089670..bdf1262 100644 --- a/app/Pog/lib_compiled/Pog/src/Commands/ContainerCommands/InstallFromUrlCommand.cs +++ b/app/Pog/lib_compiled/Pog/src/Commands/ContainerCommands/InstallFromUrlCommand.cs @@ -8,6 +8,7 @@ using Pog.InnerCommands; using Pog.InnerCommands.Common; using Pog.Utils; +using PPaths = Pog.PathConfig.PackagePaths; namespace Pog.Commands.ContainerCommands; @@ -35,11 +36,11 @@ public class InstallFromUrlCommand : PogCmdlet { protected override void BeginProcessing() { base.BeginProcessing(); _packageDirPath = SessionState.Path.CurrentLocation.ProviderPath; - _appDirPath = $@"{_packageDirPath}\{PathConfig.PackagePaths.AppDirName}"; - _newAppDirPath = $@"{_packageDirPath}\{PathConfig.PackagePaths.NewAppDirName}"; - _oldAppDirPath = $@"{_packageDirPath}\{PathConfig.PackagePaths.AppBackupDirName}"; - _extractionDirPath = $@"{_packageDirPath}\{PathConfig.PackagePaths.TmpExtractionDirName}"; - _tmpDeletePath = $@"{_packageDirPath}\{PathConfig.PackagePaths.TmpDeleteDirName}"; + _appDirPath = $@"{_packageDirPath}\{PPaths.AppDirName}"; + _newAppDirPath = $@"{_packageDirPath}\{PPaths.NewAppDirName}"; + _oldAppDirPath = $@"{_packageDirPath}\{PPaths.AppBackupDirName}"; + _extractionDirPath = $@"{_packageDirPath}\{PPaths.TmpExtractionDirName}"; + _tmpDeletePath = $@"{_packageDirPath}\{PPaths.TmpDeleteDirName}"; // read download parameters from the global container info variable var internalInfo = Container.ContainerInternalInfo.GetCurrent(this); @@ -99,7 +100,7 @@ protected override void ProcessRecord() { PackageInstallParametersArchive pa => pa.Target, _ => throw new UnreachableException(), }; - var targetPath = target == null ? _newAppDirPath : JoinValidateSubdirectory(_newAppDirPath, target); + var targetPath = target == null ? _newAppDirPath : FsUtils.JoinValidateSubdirectory(_newAppDirPath, target); if (targetPath == null) { // the target path escapes from the app directory @@ -152,21 +153,6 @@ public override void Dispose() { // (and if it isn't, it probably also won't work here) } - /// Assumes that both paths are resolved and cleaned. - private static bool EscapesDirectory(string basePath, string validatedPath) { - return !validatedPath.StartsWith(basePath + '\\') && validatedPath != basePath; - } - - /// If `subdirectoryPath` stays inside `basePath`, return the combined path, otherwise return null. - /// Use this function when resolving an untrusted relative path. - private static string? JoinValidateSubdirectory(string basePath, string subdirectoryPath) { - var combined = Path.GetFullPath(basePath + '\\' + subdirectoryPath.TrimEnd('/', '\\')); - if (EscapesDirectory(basePath, combined)) { - return null; - } - return combined; - } - private void InstallNoArchive(SharedFileCache.IFileLock downloadedFile, string targetPath) { // ensure that the parent directory exists Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); @@ -182,7 +168,7 @@ private void InstallArchive(PackageInstallParametersArchive param, InvokePogCommand(new ExpandArchive7Zip(this) { ArchivePath = downloadedFile.Path, TargetPath = _extractionDirPath, - Filter = param.Subdirectory == null ? null : new[] {param.Subdirectory}, + Filter = param.Subdirectory == null ? null : [param.Subdirectory], }); } @@ -242,7 +228,7 @@ private DirectoryInfo GetExtractedSubdirectory(string extractedRootPath, string? var subPath = resolvedPaths[0]!; - if (EscapesDirectory(extractedRootPath, subPath)) { + if (FsUtils.EscapesDirectory(extractedRootPath, subPath)) { ThrowTerminatingArgumentError(subdirectory, "SubdirectoryEscapesRoot", $"Argument passed to the -Subdirectory parameter must be a relative path that does not escape " + $"the archive directory, got '{subdirectory}'."); @@ -250,8 +236,6 @@ private DirectoryInfo GetExtractedSubdirectory(string extractedRootPath, string? // test if the path exists in the extracted directory var sub = new DirectoryInfo(subPath); - if ((int) sub.Attributes == -1) {} - if ((sub.Attributes & FileAttributes.Directory) == 0) { // it's actually a file // use the parent directory, 7zip should have only extracted the file we're interested in diff --git a/app/Pog/lib_compiled/Pog/src/FsUtils.cs b/app/Pog/lib_compiled/Pog/src/FsUtils.cs index de33fd1..293c6d2 100644 --- a/app/Pog/lib_compiled/Pog/src/FsUtils.cs +++ b/app/Pog/lib_compiled/Pog/src/FsUtils.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -76,6 +78,16 @@ public static bool FileContentEqual(byte[] f1, FileInfo f2) { return f1.Length == f2.Length && f1.SequenceEqual(File.ReadAllBytes(f2.FullName)); } + public static bool FileContentEqual(ZipArchiveEntry f1, FileInfo f2) { + if (f1.Length != f2.Length) return false; + + var f1Content = new byte[f1.Length]; + using var f1Stream = f1.Open(); + var bytesRead = f1Stream.Read(f1Content, 0, (int) f1.Length); + Debug.Assert(bytesRead == f1.Length); + return f1Content.SequenceEqual(File.ReadAllBytes(f2.FullName)); + } + public static bool FileContentEqual(FileInfo f1, FileInfo f2) { // significantly faster than trying to do a streaming implementation return f1.Length == f2.Length && File.ReadAllBytes(f1.FullName).SequenceEqual(File.ReadAllBytes(f2.FullName)); @@ -169,6 +181,8 @@ public static bool EnsureDeleteFile(string filePath) { return true; } catch (FileNotFoundException) { return false; + } catch (DirectoryNotFoundException) { + return false; // thrown when parent dir does not exist } } @@ -302,4 +316,20 @@ public static bool IsDirectoryLocked(string directoryPath) { // move directory to itself; this returns false when the directory contains anything locked, and is a no-op otherwise return !MoveDirectoryUnlocked(directoryPath, directoryPath); } + + /// Assumes that both paths are resolved and cleaned. + public static bool EscapesDirectory(string basePath, string validatedPath) { + return !validatedPath.StartsWith(basePath + '\\') && validatedPath != basePath; + } + + /// If `subdirectoryPath` stays inside `basePath`, return the combined path, otherwise return null. + /// Assumes that is resolved in canonical form. + /// Use this function when resolving an untrusted relative path. + public static string? JoinValidateSubdirectory(string basePath, string subdirectoryPath) { + var combined = Path.GetFullPath(basePath + '\\' + subdirectoryPath.TrimEnd('/', '\\')); + if (EscapesDirectory(basePath, combined)) { + return null; + } + return combined; + } } diff --git a/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeCachedFileDownload.cs b/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeCachedFileDownload.cs index bb3ae1d..fd7042c 100644 --- a/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeCachedFileDownload.cs +++ b/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeCachedFileDownload.cs @@ -26,7 +26,7 @@ public enum UserAgentType { } } -public class InvokeCachedFileDownload : ScalarCommand { +public class InvokeCachedFileDownload(PogCmdlet cmdlet) : ScalarCommand(cmdlet) { [Parameter(Mandatory = true)] public string SourceUrl = null!; [Parameter(Mandatory = true)] public string? ExpectedHash; [Parameter(Mandatory = true)] public DownloadParameters DownloadParameters = null!; @@ -34,8 +34,6 @@ public class InvokeCachedFileDownload : ScalarCommand [Parameter] public bool StoreInCache = false; [Parameter] public CmdletProgressBar.ProgressActivity ProgressActivity = new(); - public InvokeCachedFileDownload(PogCmdlet cmdlet) : base(cmdlet) {} - // TODO: handle `InvalidCacheEntryException` everywhere public override SharedFileCache.IFileLock Invoke() { Debug.Assert(ExpectedHash == null || !StoreInCache); @@ -46,7 +44,7 @@ public override SharedFileCache.IFileLock Invoke() { if (ExpectedHash != null) { WriteDebug($"Checking if we have a cached copy for '{ExpectedHash}'..."); - var entryLock = GetEntryLockedWithCleanup(ExpectedHash, Package); + var entryLock = GetEntryLockedWithCleanup(ExpectedHash); if (entryLock != null) { WriteInformation($"File retrieved from the local cache: '{SourceUrl}'"); // do not validate the hash; it was already validated once when the entry was first downloaded; @@ -108,7 +106,7 @@ private SharedFileCache.CacheEntryLock AddEntryToCache(string hash, SharedFileCa return InternalState.DownloadCache.AddEntryLocked(hash, entry); } catch (CacheEntryAlreadyExistsException) { WriteVerbose("File is already cached."); - var entryLock = GetEntryLockedWithCleanup(hash, Package); + var entryLock = GetEntryLockedWithCleanup(hash); if (entryLock == null) { continue; // retry } @@ -126,9 +124,9 @@ private SharedFileCache.CacheEntryLock AddEntryToCache(string hash, SharedFileCa } } - private SharedFileCache.CacheEntryLock? GetEntryLockedWithCleanup(string hash, Package package) { + private SharedFileCache.CacheEntryLock? GetEntryLockedWithCleanup(string hash) { try { - return InternalState.DownloadCache.GetEntryLocked(hash, package); + return InternalState.DownloadCache.GetEntryLocked(hash, Package); } catch (InvalidCacheEntryException) { WriteWarning($"Found an invalid download cache entry '{hash}', replacing..."); // TODO: figure out how to handle the failure more gracefully (maybe skip the cache all-together diff --git a/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeContainer.cs b/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeContainer.cs index f95edf1..8b2fd3c 100644 --- a/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeContainer.cs +++ b/app/Pog/lib_compiled/Pog/src/InnerCommands/InvokeContainer.cs @@ -6,7 +6,7 @@ namespace Pog.InnerCommands; -public class InvokeContainer : EnumerableCommand { +public class InvokeContainer(PogCmdlet cmdlet) : EnumerableCommand(cmdlet) { [Parameter(Mandatory = true)] public Container.ContainerType ContainerType; [Parameter(Mandatory = true)] public Package Package = null!; [Parameter] public Hashtable? InternalArguments; @@ -14,8 +14,6 @@ public class InvokeContainer : EnumerableCommand { private Container? _container; - public InvokeContainer(PogCmdlet cmdlet) : base(cmdlet) {} - public override IEnumerable Invoke() { _container = new Container(ContainerType, Package, InternalArguments, PackageArguments, Host, ReadStreamPreferenceVariables()); diff --git a/app/Pog/lib_compiled/Pog/src/Pog.Container.cs b/app/Pog/lib_compiled/Pog/src/Pog.Container.cs index 6e09b7d..6feb69c 100644 --- a/app/Pog/lib_compiled/Pog/src/Pog.Container.cs +++ b/app/Pog/lib_compiled/Pog/src/Pog.Container.cs @@ -34,7 +34,8 @@ public sealed class Container : IDisposable { /// To read the output streams, use the property after was called. /// /// Configuration for output stream preference variables. - public Container(ContainerType containerType, Package package, Hashtable? internalArguments, Hashtable? packageArguments, + public Container(ContainerType containerType, Package package, + Hashtable? internalArguments, Hashtable? packageArguments, PSHost? host, OutputStreamConfig streamConfig) { _containerType = containerType; _package = package; @@ -141,7 +142,7 @@ private InitialSessionState CreateInitialSessionState() { }); var containerDir = InternalState.PathConfig.ContainerDir; - iss.ImportPSModule(new[] { + iss.ImportPSModule([ // these two imports contain basic stuff needed for printing output, errors, FS traversal,... "Microsoft.PowerShell.Management", "Microsoft.PowerShell.Utility", @@ -154,7 +155,7 @@ private InitialSessionState CreateInitialSessionState() { ContainerType.GetInstallHash => $@"{containerDir}\Install\Env_GetInstallHash.psm1", _ => throw new ArgumentOutOfRangeException(nameof(_containerType), _containerType, null), }, - }); + ]); // TODO: figure out if we can define this without having to write inline PowerShell function // override Import-Module to hide the default verbose prints when -Verbose is set for the container environment diff --git a/app/Pog/lib_compiled/Pog/src/Pog.SharedFileCache.cs b/app/Pog/lib_compiled/Pog/src/Pog.SharedFileCache.cs index 8202bef..c7856cb 100644 --- a/app/Pog/lib_compiled/Pog/src/Pog.SharedFileCache.cs +++ b/app/Pog/lib_compiled/Pog/src/Pog.SharedFileCache.cs @@ -45,17 +45,12 @@ public class CacheEntryInUseException(string entryKey) // - second one has any other name, and is the actual cache entry; during insertion, if the entry file is also called // `referencingPackages.json-list`, it is prefixed with `_` [PublicAPI] -public class SharedFileCache { +public class SharedFileCache(string cacheDirPath, TmpDirectory tmpDir) { private const string MetadataFileName = "referencingPackages.json-list"; - public readonly string Path; + public readonly string Path = cacheDirPath; /// Directory for temporary files on the same volume as `.Path`, used for adding and removing entries. - private readonly TmpDirectory _tmpDirectory; - - public SharedFileCache(string cacheDirPath, TmpDirectory tmpDir) { - Path = cacheDirPath; - _tmpDirectory = tmpDir; - } + private readonly TmpDirectory _tmpDirectory = tmpDir; public delegate void InvalidCacheEntryCb(InvalidCacheEntryException exception); diff --git a/app/Pog/lib_compiled/Pog/src/Pog.TmpDirectory.cs b/app/Pog/lib_compiled/Pog/src/Pog.TmpDirectory.cs index 3108771..16eb905 100644 --- a/app/Pog/lib_compiled/Pog/src/Pog.TmpDirectory.cs +++ b/app/Pog/lib_compiled/Pog/src/Pog.TmpDirectory.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.IO; using JetBrains.Annotations; -using IOPath = System.IO.Path; namespace Pog; @@ -11,15 +10,11 @@ namespace Pog; /// because the directory must be at the same partition as the download cache. /// [PublicAPI] -public class TmpDirectory { - public readonly string Path; - - public TmpDirectory(string tmpDirPath) { - Path = tmpDirPath; - } +public class TmpDirectory(string tmpDirPath) { + public readonly string Path = tmpDirPath; public string GetTemporaryPath() { - var path = IOPath.Combine(Path, Guid.NewGuid().ToString()); + var path = $"{Path}\\{Guid.NewGuid()}"; // the chance is very small... Debug.Assert(!Directory.Exists(path) && !File.Exists(path)); return path; diff --git a/app/Pog/lib_compiled/Pog/src/Utils/FileSystemInfoExtensions.cs b/app/Pog/lib_compiled/Pog/src/Utils/FileSystemInfoExtensions.cs index c8e04db..cf1dc50 100644 --- a/app/Pog/lib_compiled/Pog/src/Utils/FileSystemInfoExtensions.cs +++ b/app/Pog/lib_compiled/Pog/src/Utils/FileSystemInfoExtensions.cs @@ -2,7 +2,7 @@ namespace Pog.Utils; -public static class FileSystemInfoExtensions { +internal static class FileSystemInfoExtensions { public static string GetBaseName(this FileSystemInfo info) { return Path.GetFileNameWithoutExtension(info.Name); } diff --git a/app/Pog/lib_compiled/Pog/src/Utils/IEnumerableExtensions.cs b/app/Pog/lib_compiled/Pog/src/Utils/IEnumerableExtensions.cs index 080e568..1160c08 100644 --- a/app/Pog/lib_compiled/Pog/src/Utils/IEnumerableExtensions.cs +++ b/app/Pog/lib_compiled/Pog/src/Utils/IEnumerableExtensions.cs @@ -4,7 +4,7 @@ namespace Pog.Utils; -public static class EnumerableExtensions { +internal static class EnumerableExtensions { public static IEnumerable SelectOptional(this IEnumerable enumerable, Func selector) where TOut : class { return enumerable.Select(selector).WhereNotNull(); diff --git a/app/Pog/lib_compiled/Pog/src/Utils/KeyValuePairExtensions.cs b/app/Pog/lib_compiled/Pog/src/Utils/KeyValuePairExtensions.cs index 468609e..a61cca3 100644 --- a/app/Pog/lib_compiled/Pog/src/Utils/KeyValuePairExtensions.cs +++ b/app/Pog/lib_compiled/Pog/src/Utils/KeyValuePairExtensions.cs @@ -2,7 +2,7 @@ namespace Pog.Utils; -public static class KeyValuePairExtensions { +internal static class KeyValuePairExtensions { public static void Deconstruct(this KeyValuePair keyValuePair, out TKey key, out TValue value) { key = keyValuePair.Key; diff --git a/app/Pog/lib_compiled/Pog/src/Utils/LazyDisposable.cs b/app/Pog/lib_compiled/Pog/src/Utils/LazyDisposable.cs index af5b63d..839bede 100644 --- a/app/Pog/lib_compiled/Pog/src/Utils/LazyDisposable.cs +++ b/app/Pog/lib_compiled/Pog/src/Utils/LazyDisposable.cs @@ -7,7 +7,7 @@ namespace Pog.Utils; /// A object that implements . /// The object being lazily created. -public sealed class LazyDisposable : Lazy, IDisposable where T : IDisposable { +internal sealed class LazyDisposable : Lazy, IDisposable where T : IDisposable { /// /// Initializes a new instance of the class. /// When lazy initialization occurs, the default constructor is used.