Skip to content

Feature: Added support for changing encoding when extracting ZIP files #17022

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

Merged
merged 13 commits into from
Apr 9, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="OwlCore.Storage" Version="0.12.2" />
<PackageVersion Include="Sentry" Version="5.1.1" />
<PackageVersion Include="SevenZipSharp" Version="1.0.2" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_green" Version="2.1.10" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ public override async Task ExecuteAsync(object? parameter = null)
return;

var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path));
var isArchiveEncodingUndetermined = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncodingUndeterminedAsync(archive.Path));
var password = string.Empty;
Encoding? encoding = null;

DecompressArchiveDialog decompressArchiveDialog = new();
DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive)
{
IsArchiveEncrypted = isArchiveEncrypted,
IsArchiveEncodingUndetermined = isArchiveEncodingUndetermined,
ShowPathSelection = true
};
decompressArchiveDialog.ViewModel = decompressArchiveViewModel;
Expand All @@ -62,6 +65,8 @@ public override async Task ExecuteAsync(object? parameter = null)
if (isArchiveEncrypted && decompressArchiveViewModel.Password is not null)
password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password);

encoding = decompressArchiveViewModel.SelectedEncoding.Encoding;

// Check if archive still exists
if (!StorageHelpers.Exists(archive.Path))
return;
Expand All @@ -77,7 +82,7 @@ public override async Task ExecuteAsync(object? parameter = null)

// Operate decompress
var result = await FilesystemTasks.Wrap(() =>
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password));
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password, encoding));

if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion)
await NavigationHelpers.OpenPath(destinationFolderPath, context.ShellPage, FilesystemItemType.Directory);
Expand Down
11 changes: 10 additions & 1 deletion src/Files.App/Data/Contracts/IStorageArchiveService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using System.Text;
using SevenZip;

namespace Files.App.Data.Contracts
Expand Down Expand Up @@ -37,8 +38,9 @@ public interface IStorageArchiveService
/// <param name="archiveFilePath">The archive file path to decompress.</param>
/// <param name="destinationFolderPath">The destination folder path which the archive file will be decompressed to.</param>
/// <param name="password">The password to decrypt the archive file if applicable.</param>
/// <param name="encoding">The file name encoding to decrypt the archive file. If set to null, system default encoding will be used.</param>
/// <returns>True if the decompression has done successfully; otherwise, false.</returns>
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "");
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null);

/// <summary>
/// Generates the archive file name from item names.
Expand All @@ -54,6 +56,13 @@ public interface IStorageArchiveService
/// <returns>True if the archive file is encrypted; otherwise, false.</returns>
Task<bool> IsEncryptedAsync(string archiveFilePath);

/// <summary>
/// Gets the value that indicates whether the archive file's encoding is undetermined.
/// </summary>
/// <param name="archiveFilePath">The archive file path to check if the item is encrypted.</param>
/// <returns>True if the archive file's encoding is undetermined; otherwise, false.</returns>
Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath);

/// <summary>
/// Gets the <see cref="SevenZipExtractor"/> instance from the archive file path.
/// </summary>
Expand Down
41 changes: 41 additions & 0 deletions src/Files.App/Data/Items/EncodingItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.Text;

namespace Files.App.Data.Items
{
/// <summary>
/// Represents a text encoding in the application.
/// </summary>
public sealed class EncodingItem
{

public Encoding? Encoding { get; set; }

/// <summary>
/// Gets the encoding name. e.g. English (United States)
/// </summary>
public string Name { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="EncodingItem"/> class.
/// </summary>
/// <param name="code">The code of the language.</param>
public EncodingItem(string code)
{
if (string.IsNullOrEmpty(code))
{
Encoding = null;
Name = Strings.Default.GetLocalizedResource();
}
else
{
Encoding = Encoding.GetEncoding(code);
Name = Encoding.EncodingName;
}
}

public override string ToString() => Name;
}
}
30 changes: 30 additions & 0 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
<ContentDialog
x:Class="Files.App.Dialogs.DecompressArchiveDialog"
Expand All @@ -5,7 +5,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:items="using:Files.App.Data.Items"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:Files.App.UserControls"
Title="{helpers:ResourceString Name=ExtractArchive}"
CornerRadius="{StaticResource OverlayCornerRadius}"
DefaultButton="Primary"
Expand Down Expand Up @@ -80,6 +82,34 @@

</StackPanel>

<!-- Encoding -->
<StackPanel
x:Name="EncodingStackPanel"
x:Load="{x:Bind ViewModel.IsArchiveEncodingUndetermined, Mode=OneWay}"
Orientation="Vertical"
Spacing="8">

<TextBlock
x:Name="EncodingHeader"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Text="{helpers:ResourceString Name=Encoding}" />

<ComboBox
x:Name="EncodingBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.EncodingOptions, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedEncoding, Mode=TwoWay}">
<uc:ComboBoxEx.ItemTemplate>
<DataTemplate x:DataType="items:EncodingItem">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</uc:ComboBoxEx.ItemTemplate>
</uc:ComboBoxEx>

</StackPanel>

<!-- Open when complete -->
<CheckBox
x:Name="OpenDestination"
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="Sentry" />
<PackageReference Include="SevenZipSharp" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="SQLitePCLRaw.bundle_green" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Graphics.Win2D" />
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using System.IO;
using System.Text;
using Windows.ApplicationModel.Activation;
using Windows.Storage;
using static Files.App.Helpers.Win32PInvoke;
Expand Down Expand Up @@ -55,6 +56,7 @@ static Program()
[STAThread]
private static void Main()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oxygen-dioxide apologies only mentioning this now, but this should really go under AppLifecycleHelper.

WinRT.ComWrappersSupport.InitializeComWrappers();

// We are about to do the first WinRT server call, in case the WinRT server is hanging
Expand Down
159 changes: 158 additions & 1 deletion src/Files.App/Services/Storage/StorageArchiveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// Licensed under the MIT License.

using Files.Shared.Helpers;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using SevenZip;
using System.IO;
using System.Linq;
using System.Text;
using Windows.Storage;
using Windows.Win32;

Expand Down Expand Up @@ -84,7 +88,17 @@ public async Task<bool> CompressAsync(ICompressArchiveModel compressionModel)
}

/// <inheritdoc/>
public async Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "")
public Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null)
{
if(encoding == null){
return DecompressAsyncWithSevenZip(archiveFilePath, destinationFolderPath, password);
}
else
{
return DecompressAsyncWithSharpZipLib(archiveFilePath, destinationFolderPath, password, encoding);
}
}
async Task<bool> DecompressAsyncWithSevenZip(string archiveFilePath, string destinationFolderPath, string password = "")
{
if (string.IsNullOrEmpty(archiveFilePath) ||
string.IsNullOrEmpty(destinationFolderPath))
Expand Down Expand Up @@ -180,10 +194,134 @@ public async Task<bool> DecompressAsync(string archiveFilePath, string destinati
fsProgress.Report();
}
};
return isSuccess;
}

async Task<bool> DecompressAsyncWithSharpZipLib(string archiveFilePath, string destinationFolderPath, string password, Encoding encoding)
{
if (string.IsNullOrEmpty(archiveFilePath) ||
string.IsNullOrEmpty(destinationFolderPath))
return false;
using var zipFile = new ZipFile(archiveFilePath, StringCodec.FromEncoding(encoding));
if(zipFile is null)
return false;

if(!string.IsNullOrEmpty(password))
zipFile.Password = password;

// Initialize a new in-progress status card
var statusCard = StatusCenterHelper.AddCard_Decompress(
archiveFilePath.CreateEnumerable(),
destinationFolderPath.CreateEnumerable(),
ReturnResult.InProgress);

// Check if the decompress operation canceled
if (statusCard.CancellationToken.IsCancellationRequested)
return false;

StatusCenterItemProgressModel fsProgress = new(
statusCard.ProgressEventSource,
enumerationCompleted: true,
FileSystemStatusCode.InProgress,
zipFile.Cast<ZipEntry>().Count<ZipEntry>(x => !x.IsDirectory));
fsProgress.TotalSize = zipFile.Cast<ZipEntry>().Select(x => (long)x.Size).Sum();
fsProgress.Report();

bool isSuccess = false;

try
{
long processedBytes = 0;
int processedFiles = 0;

foreach (ZipEntry zipEntry in zipFile)
{
if (statusCard.CancellationToken.IsCancellationRequested)
{
isSuccess = false;
break;
}

if (!zipEntry.IsFile)
{
continue; // Ignore directories
}

string entryFileName = zipEntry.Name;
string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName);
string directoryName = Path.GetDirectoryName(fullZipToPath);

if (!Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}

byte[] buffer = new byte[4096]; // 4K is a good default
using (Stream zipStream = zipFile.GetInputStream(zipEntry))
using (FileStream streamWriter = File.Create(fullZipToPath))
{
await ThreadingService.ExecuteOnUiThreadAsync(() =>
{
fsProgress.FileName = entryFileName;
fsProgress.Report();
});

StreamUtils.Copy(zipStream, streamWriter, buffer);
}
processedBytes += zipEntry.Size;
if (fsProgress.TotalSize > 0)
{
fsProgress.Report(processedBytes / (double)fsProgress.TotalSize * 100);
}
processedFiles++;
fsProgress.AddProcessedItemsCount(1);
fsProgress.Report();
}

if (!statusCard.CancellationToken.IsCancellationRequested)
{
isSuccess = true;
}
}
catch (Exception ex)
{
isSuccess = false;
Console.WriteLine($"Error during decompression: {ex.Message}");
}
finally
{
// Remove the in-progress status card
StatusCenterViewModel.RemoveItem(statusCard);

if (isSuccess)
{
// Successful
StatusCenterHelper.AddCard_Decompress(
archiveFilePath.CreateEnumerable(),
destinationFolderPath.CreateEnumerable(),
ReturnResult.Success);
}
else
{
// Error
StatusCenterHelper.AddCard_Decompress(
archiveFilePath.CreateEnumerable(),
destinationFolderPath.CreateEnumerable(),
statusCard.CancellationToken.IsCancellationRequested
? ReturnResult.Cancelled
: ReturnResult.Failed);
}

if (zipFile != null)
{
zipFile.IsStreamOwner = true; // Makes close also close the underlying stream
zipFile.Close();
}
}
return isSuccess;
}


/// <inheritdoc/>
public string GenerateArchiveNameFromItems(IReadOnlyList<ListedItem> items)
{
Expand All @@ -208,6 +346,25 @@ public async Task<bool> IsEncryptedAsync(string archiveFilePath)
return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES"));
}

/// <inheritdoc/>
public async Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath)
{
if (archiveFilePath is null) return false;
if (Path.GetExtension(archiveFilePath) != ".zip") return false;
try
{
using (ZipFile zipFile = new ZipFile(archiveFilePath))
{
return !zipFile.Cast<ZipEntry>().All(entry=>entry.IsUnicodeText);
}
}
catch (Exception ex)
{
Console.WriteLine($"SharpZipLib error: {ex.Message}");
return true;
}
}

/// <inheritdoc/>
public async Task<SevenZipExtractor?> GetSevenZipExtractorAsync(string archiveFilePath, string password = "")
{
Expand Down
3 changes: 3 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,9 @@
<data name="ArchivePassword" xml:space="preserve">
<value>Archive password</value>
</data>
<data name="ArchiveEncoding" xml:space="preserve">
<value>Encoding</value>
</data>
<data name="ExtractToPath" xml:space="preserve">
<value>Path</value>
</data>
Expand Down
Loading
Loading