public async Task Unpack(VirtualFile file, VirtualDirectory destination, IProgressNotifier parentProgress = null, ChangeObservable observable = null) { if (Path.GetExtension(file.Name)?.Equals(".zip", StringComparison.OrdinalIgnoreCase) == true) { await UnpackZipFile().ConfigureAwait(false); } else if (file.Name.EndsWith(".tar.xz", StringComparison.OrdinalIgnoreCase)) { UnpackTarXzFile(file); } else if (Path.GetExtension(file.Name)?.Equals(".xz", StringComparison.OrdinalIgnoreCase) == true) { UnpackXzFile(file); } else if (Path.GetExtension(file.Name)?.Equals(".tar", StringComparison.OrdinalIgnoreCase) == true) { UnpackTarFile(file); } else if (Path.GetExtension(file.Name)?.Equals(".sh", StringComparison.OrdinalIgnoreCase) == true && environmentService.Platform != OSPlatform.Windows) { await UnpackSelfExtractingShellScript().ConfigureAwait(false); } else { throw new UnsupportedArchiveFormatException(file.FullName); } async Task UnpackSelfExtractingShellScript() { ValidateShellScript(); StringBuilderUserInterface userInterface = new StringBuilderUserInterface(log, writeInformation: true, writeError: true); try { using (parentProgress?.SpawnInfiniteProgress("Executing the shell script.")) using (IProcess process = processManager.StartProcess(file.FullName, $"-y -d \"{destination.FullName}\"", userInterface)) { await process.WaitForExitAsync().ConfigureAwait(false); if (process.ExitCode != 0) { throw new UnsupportedArchiveFormatException(file.FullName, new FormattableException( $"An error occured while executing the script.{Environment.NewLine}" + $"{userInterface.Error}")); } } } catch (Exception e) { if (e is FormattableException) { throw; } throw new UnsupportedArchiveFormatException(file.FullName, new FormattableException( $"An exception occured while executing the script.{Environment.NewLine}" + $"{e.Message}", e)); } void ValidateShellScript() { StringBuilderUserInterface validationUserInterface = new StringBuilderUserInterface(log, writeInformation: true, writeError: true); try { using (IProcess process = processManager.StartProcess(file.FullName, "--help", validationUserInterface)) { process.WaitForExit(); } } catch (UnauthorizedAccessException e) { throw new UnsupportedArchiveFormatException(file.FullName, new FormattableException( $"An exception occured while inspecting the script.{Environment.NewLine}" + $"This excpetion can occur when the file is not marked as executable.{Environment.NewLine}" + $"{e.Message}", e)); } catch (Exception e) { if (e is FormattableException) { throw; } throw new UnsupportedArchiveFormatException(file.FullName, new FormattableException( $"An exception occured while inspecting the script.{Environment.NewLine}" + $"{e.Message}", e)); } if (!Regex.IsMatch(validationUserInterface.Information, @"(?=.*(?:usage|Usage)).*(?=.*-y)(?=.*-d)")) { throw new UnsupportedArchiveFormatException(file.FullName, new FormattableException($"Did not find the expected usage information.{Environment.NewLine}" + $"The expected information need to include the options '-y' and '-d'.{Environment.NewLine}" + $"The following usage information was given:{Environment.NewLine}" + $"{validationUserInterface.Information}")); } } } async Task UnpackZipFile() { using (Stream fileStream = file.OpenRead()) using (ZipFile zipFile = new ZipFile(fileStream)) using (IProgressNotifier mainProgress = parentProgress?.Spawn(2, $"Extracting {file.FullName} to {destination.FullName}.")) { archiveResultBuilder.Clear(); using (mainProgress?.SpawnInfiniteProgress("Test archive integrity.")) { ZipFile copy = zipFile; await Task.Run(() => { if (!copy.TestArchive(true, TestStrategy.FindAllErrors, ResultHandler)) { throw new UnsupportedArchiveFormatException( file.FullName, new FormattableException(archiveResultBuilder.ToString())); } }).ConfigureAwait(false); } double increment = (double)Constants.ProgressMaxResolution / zipFile.Count + 1; using (IProgressNotifier extractProgress = mainProgress?.Spawn(Constants.ProgressMaxResolution, "Extract files")) { foreach (ZipEntry zipEntry in zipFile) { extractProgress?.TickIncrement(increment); if (!zipEntry.IsFile) { continue; // Ignore directories } byte[] buffer = new byte[Constants.StreamCopyBufferSize]; // 4K is optimum using (Stream zipStream = zipFile.GetInputStream(zipEntry)) { string[] path = fileSystem.GetPath(zipEntry.Name); VirtualDirectory fileDestination = destination.Directory(path.Take(path.Length - 1).ToArray()); VirtualFile entryFile = fileDestination.File(path.Last()); // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size // of the file, but does not waste memory. using (Stream streamWriter = entryFile.OpenWrite()) { StreamUtils.Copy(zipStream, streamWriter, buffer); } } } } } } VirtualFile UnpackXzFile(VirtualFile packedFile) { using (Stream fileStream = packedFile.OpenRead()) using (XZStream xzStream = new XZStream(fileStream)) using (parentProgress?.SpawnInfiniteProgress($"Extracting .xz file {packedFile.Name}...")) { string[] path = fileSystem.GetPath(packedFile.FullName); string relativeFilePath = path.Last().Substring(0, path.Last().LastIndexOf(".xz", StringComparison.OrdinalIgnoreCase)); if (destination.FileExists(relativeFilePath)) { destination.File(relativeFilePath).Delete(); } VirtualFile destinationFile = destination.File(relativeFilePath); observable?.OnNext(new Change(() => destinationFile.Restore())); byte[] buffer = new byte[Constants.StreamCopyBufferSize]; // 4K is optimum using (Stream streamWriter = destinationFile.OpenWrite()) { StreamUtils.Copy(xzStream, streamWriter, buffer); return(destinationFile); } } } void UnpackTarFile(VirtualFile packedFile) { //sharpcompress using (Stream fileStream = packedFile.OpenRead()) using (TarArchive tarArchive = TarArchive.Open(fileStream)) { double entriesUntilTickIncrement = tarArchive.Entries.Count / (double)Constants.ProgressUpdateInterval; using (IProgressNotifier extractProgress = parentProgress?.Spawn(Constants.ProgressMaxResolution, "Extracting .tar archive")) { int entriesSinceLastTickIncrement = 0; foreach (TarArchiveEntry tarEntry in tarArchive.Entries) { if (entriesSinceLastTickIncrement > entriesUntilTickIncrement) { extractProgress?.TickIncrement(Constants.ProgressUpdateInterval); entriesSinceLastTickIncrement = 0; } if (tarEntry.IsDirectory) { continue; // Ignore directories } byte[] buffer = new byte[Constants.StreamCopyBufferSize]; // 4K is optimum using (Stream tarStream = tarEntry.OpenEntryStream()) { string[] path = fileSystem.GetPath(tarEntry.Key); VirtualDirectory fileDestination = destination.Directory(path.Take(path.Length - 1).ToArray()); if (fileDestination.FileExists(path.Last())) { fileDestination.File(path.Last()).Delete(); } VirtualFile entryFile = fileDestination.File(path.Last()); observable?.OnNext(new Change(() => entryFile.Restore() //, $"Unpack {tarEntry.Key}." )); //Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size // of the file, but does not waste memory. using (Stream streamWriter = entryFile.OpenWrite()) { StreamUtils.Copy(tarStream, streamWriter, buffer); } } entriesSinceLastTickIncrement++; } extractProgress?.Tick(Constants.ProgressMaxResolution); } } } void UnpackTarXzFile(VirtualFile packedFile) { using (IProgressNotifier subProgress = parentProgress?.Spawn(2, "Extracting tar.xz file")) { parentProgress = subProgress; VirtualFile tarFile = UnpackXzFile(packedFile); UnpackTarFile(tarFile); tarFile.Delete(); } } }