diff --git a/Directory.Packages.props b/Directory.Packages.props
index 81af77f22b58..d3c069caa7e7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -32,6 +32,7 @@
+
diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs
index a01eaa3ee536..4f7ef9d4ae40 100644
--- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs
+++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs
@@ -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;
@@ -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;
@@ -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);
diff --git a/src/Files.App/Data/Contracts/IStorageArchiveService.cs b/src/Files.App/Data/Contracts/IStorageArchiveService.cs
index 1052125ce44f..27487eb1763c 100644
--- a/src/Files.App/Data/Contracts/IStorageArchiveService.cs
+++ b/src/Files.App/Data/Contracts/IStorageArchiveService.cs
@@ -1,6 +1,7 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
+using System.Text;
using SevenZip;
namespace Files.App.Data.Contracts
@@ -37,8 +38,9 @@ public interface IStorageArchiveService
/// The archive file path to decompress.
/// The destination folder path which the archive file will be decompressed to.
/// The password to decrypt the archive file if applicable.
+ /// The file name encoding to decrypt the archive file. If set to null, system default encoding will be used.
/// True if the decompression has done successfully; otherwise, false.
- Task DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "");
+ Task DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null);
///
/// Generates the archive file name from item names.
@@ -54,6 +56,13 @@ public interface IStorageArchiveService
/// True if the archive file is encrypted; otherwise, false.
Task IsEncryptedAsync(string archiveFilePath);
+ ///
+ /// Gets the value that indicates whether the archive file's encoding is undetermined.
+ ///
+ /// The archive file path to check if the item is encrypted.
+ /// True if the archive file's encoding is undetermined; otherwise, false.
+ Task IsEncodingUndeterminedAsync(string archiveFilePath);
+
///
/// Gets the instance from the archive file path.
///
diff --git a/src/Files.App/Data/Items/EncodingItem.cs b/src/Files.App/Data/Items/EncodingItem.cs
new file mode 100644
index 000000000000..c342c64fc9c8
--- /dev/null
+++ b/src/Files.App/Data/Items/EncodingItem.cs
@@ -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
+{
+ ///
+ /// Represents a text encoding in the application.
+ ///
+ public sealed class EncodingItem
+ {
+
+ public Encoding? Encoding { get; set; }
+
+ ///
+ /// Gets the encoding name. e.g. English (United States)
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The code of the language.
+ 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;
+ }
+}
diff --git a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml
index 84363e781489..732f04e7072d 100644
--- a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml
+++ b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml
@@ -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"
@@ -80,6 +82,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Files.App/Program.cs b/src/Files.App/Program.cs
index 79f87e1ed322..6d7807095797 100644
--- a/src/Files.App/Program.cs
+++ b/src/Files.App/Program.cs
@@ -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;
@@ -55,6 +56,7 @@ static Program()
[STAThread]
private static void Main()
{
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
WinRT.ComWrappersSupport.InitializeComWrappers();
// We are about to do the first WinRT server call, in case the WinRT server is hanging
diff --git a/src/Files.App/Services/Storage/StorageArchiveService.cs b/src/Files.App/Services/Storage/StorageArchiveService.cs
index f3aab26e34f7..1b30e1adaddf 100644
--- a/src/Files.App/Services/Storage/StorageArchiveService.cs
+++ b/src/Files.App/Services/Storage/StorageArchiveService.cs
@@ -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;
@@ -84,7 +88,17 @@ public async Task CompressAsync(ICompressArchiveModel compressionModel)
}
///
- public async Task DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "")
+ public Task 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 DecompressAsyncWithSevenZip(string archiveFilePath, string destinationFolderPath, string password = "")
{
if (string.IsNullOrEmpty(archiveFilePath) ||
string.IsNullOrEmpty(destinationFolderPath))
@@ -180,10 +194,134 @@ public async Task DecompressAsync(string archiveFilePath, string destinati
fsProgress.Report();
}
};
+ return isSuccess;
+ }
+
+ async Task 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().Count(x => !x.IsDirectory));
+ fsProgress.TotalSize = zipFile.Cast().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;
}
+
///
public string GenerateArchiveNameFromItems(IReadOnlyList items)
{
@@ -208,6 +346,25 @@ public async Task IsEncryptedAsync(string archiveFilePath)
return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES"));
}
+ ///
+ public async Task 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().All(entry=>entry.IsUnicodeText);
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"SharpZipLib error: {ex.Message}");
+ return true;
+ }
+ }
+
///
public async Task GetSevenZipExtractorAsync(string archiveFilePath, string password = "")
{
diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index a464faec9838..6abdb2f79735 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -2096,6 +2096,9 @@
Archive password
+
+ Encoding
+
Path
diff --git a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs
index 6f3151f323de..b1ff52bc2b35 100644
--- a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs
+++ b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.IO;
+using System.Text;
using System.Windows.Input;
using Windows.Storage;
@@ -36,6 +37,13 @@ public bool IsArchiveEncrypted
set => SetProperty(ref isArchiveEncrypted, value);
}
+ private bool isArchiveEncodingUndetermined;
+ public bool IsArchiveEncodingUndetermined
+ {
+ get => isArchiveEncodingUndetermined;
+ set => SetProperty(ref isArchiveEncodingUndetermined, value);
+ }
+
private bool showPathSelection;
public bool ShowPathSelection
{
@@ -45,6 +53,20 @@ public bool ShowPathSelection
public DisposableArray? Password { get; private set; }
+ public EncodingItem[] EncodingOptions { get; set; } = new string?[] {
+ null,//System Default
+ "UTF-8",
+ "shift_jis",
+ "gb2312",
+ "big5",
+ "ks_c_5601-1987",
+ "Windows-1252",
+ "macintosh",
+ }
+ .Select(x=>new EncodingItem(x))
+ .ToArray();
+ public EncodingItem SelectedEncoding { get; set; }
+
public IRelayCommand PrimaryButtonClickCommand { get; private set; }
public ICommand SelectDestinationCommand { get; private set; }
@@ -53,6 +75,7 @@ public DecompressArchiveDialogViewModel(IStorageFile archive)
{
this.archive = archive;
destinationFolderPath = DefaultDestinationFolderPath();
+ SelectedEncoding = EncodingOptions[0];
// Create commands
SelectDestinationCommand = new AsyncRelayCommand(SelectDestinationAsync);