diff --git a/Directory.Packages.props b/Directory.Packages.props index 4d36e6491fc7..67e1899c5d2c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,7 @@ + diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs index 0bff8f7ce6f3..ecff3a898fed 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs @@ -43,6 +43,11 @@ public override async Task ExecuteAsync(object? parameter = null) var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path)); var isArchiveEncodingUndetermined = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncodingUndeterminedAsync(archive.Path)); + Encoding? detectedEncoding = null; + if (isArchiveEncodingUndetermined) + { + detectedEncoding = await FilesystemTasks.Wrap(() => StorageArchiveService.DetectEncodingAsync(archive.Path)); + } var password = string.Empty; Encoding? encoding = null; @@ -51,7 +56,8 @@ public override async Task ExecuteAsync(object? parameter = null) { IsArchiveEncrypted = isArchiveEncrypted, IsArchiveEncodingUndetermined = isArchiveEncodingUndetermined, - ShowPathSelection = true + ShowPathSelection = true, + DetectedEncoding = detectedEncoding, }; decompressArchiveDialog.ViewModel = decompressArchiveViewModel; diff --git a/src/Files.App/Data/Contracts/IStorageArchiveService.cs b/src/Files.App/Data/Contracts/IStorageArchiveService.cs index 27487eb1763c..ee0ee550bfdc 100644 --- a/src/Files.App/Data/Contracts/IStorageArchiveService.cs +++ b/src/Files.App/Data/Contracts/IStorageArchiveService.cs @@ -59,10 +59,17 @@ public interface IStorageArchiveService /// /// Gets the value that indicates whether the archive file's encoding is undetermined. /// - /// The archive file path to check if the item is encrypted. + /// The archive file path to check if the encoding is undetermined. /// True if the archive file's encoding is undetermined; otherwise, false. Task IsEncodingUndeterminedAsync(string archiveFilePath); + /// + /// Detect encoding for a zip file whose encoding is undetermined. + /// + /// The archive file path to detect encoding + /// Null if the archive file doesn't need to detect encoding or its encoding can't be detected; otherwise, the encoding detected. + Task DetectEncodingAsync(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 index c342c64fc9c8..24c79bc35bf2 100644 --- a/src/Files.App/Data/Items/EncodingItem.cs +++ b/src/Files.App/Data/Items/EncodingItem.cs @@ -22,7 +22,7 @@ public sealed class EncodingItem /// Initializes a new instance of the class. /// /// The code of the language. - public EncodingItem(string code) + public EncodingItem(string? code) { if (string.IsNullOrEmpty(code)) { @@ -36,6 +36,45 @@ public EncodingItem(string code) } } - public override string ToString() => Name; + public EncodingItem(Encoding encoding, string name) + { + Encoding = encoding; + Name = name; + } + + public static EncodingItem[] Defaults = new string?[] { + null,//System Default + "UTF-8", + + //All possible Windows system encodings + //reference: https://en.wikipedia.org/wiki/Windows_code_page + //East Asian + "shift_jis", //Japanese + "gb2312", //Simplified Chinese + "big5", //Traditional Chinese + "ks_c_5601-1987", //Korean + + //Southeast Asian + "Windows-1258", //Vietnamese + "Windows-874", //Thai + + //Middle East + "Windows-1256", //Arabic + "Windows-1255", //Hebrew + "Windows-1254", //Turkish + + //European + "Windows-1252", //Western European + "Windows-1250", //Central European + "Windows-1251", //Cyrillic + "Windows-1253", //Greek + "Windows-1257", //Baltic + + "macintosh", + } + .Select(x => new EncodingItem(x)) + .ToArray(); + + public override string ToString() => Name; } } diff --git a/src/Files.App/Files.App.csproj b/src/Files.App/Files.App.csproj index 78196a615a92..6ed8fc5d9166 100644 --- a/src/Files.App/Files.App.csproj +++ b/src/Files.App/Files.App.csproj @@ -95,6 +95,7 @@ + diff --git a/src/Files.App/Services/Storage/StorageArchiveService.cs b/src/Files.App/Services/Storage/StorageArchiveService.cs index 1b30e1adaddf..bf32429177b0 100644 --- a/src/Files.App/Services/Storage/StorageArchiveService.cs +++ b/src/Files.App/Services/Storage/StorageArchiveService.cs @@ -5,9 +5,11 @@ using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Zip; using SevenZip; +using System.Collections; using System.IO; using System.Linq; using System.Text; +using UtfUnknown; using Windows.Storage; using Windows.Win32; @@ -233,51 +235,52 @@ async Task DecompressAsyncWithSharpZipLib(string archiveFilePath, string d { long processedBytes = 0; int processedFiles = 0; - - foreach (ZipEntry zipEntry in zipFile) + await Task.Run(async () => { - if (statusCard.CancellationToken.IsCancellationRequested) + foreach (ZipEntry zipEntry in zipFile) { - isSuccess = false; - break; - } - - if (!zipEntry.IsFile) - { - continue; // Ignore directories - } + if (statusCard.CancellationToken.IsCancellationRequested) + { + isSuccess = false; + break; + } - string entryFileName = zipEntry.Name; - string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName); - string directoryName = Path.GetDirectoryName(fullZipToPath); + if (!zipEntry.IsFile) + { + continue; // Ignore directories + } - if (!Directory.Exists(directoryName)) - { - Directory.CreateDirectory(directoryName); - } + string entryFileName = zipEntry.Name; + string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName); + string directoryName = Path.GetDirectoryName(fullZipToPath); - 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(() => + if (!Directory.Exists(directoryName)) { - fsProgress.FileName = entryFileName; - fsProgress.Report(); - }); + Directory.CreateDirectory(directoryName); + } - StreamUtils.Copy(zipStream, streamWriter, buffer); - } - processedBytes += zipEntry.Size; - if (fsProgress.TotalSize > 0) - { - fsProgress.Report(processedBytes / (double)fsProgress.TotalSize * 100); + 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(); } - processedFiles++; - fsProgress.AddProcessedItemsCount(1); - fsProgress.Report(); - } - + }); if (!statusCard.CancellationToken.IsCancellationRequested) { isSuccess = true; @@ -365,6 +368,42 @@ public async Task IsEncodingUndeterminedAsync(string archiveFilePath) } } + public async Task DetectEncodingAsync(string archiveFilePath) + { + //Temporarily using cp437 to decode zip file + //because SharpZipLib requires an encoding when decoding + //and cp437 contains all bytes as character + //which means that we can store any byte array as cp437 string losslessly + var cp437 = Encoding.GetEncoding(437); + try + { + using (ZipFile zipFile = new ZipFile(archiveFilePath, StringCodec.FromEncoding(cp437))) + { + var fileNameBytes = cp437.GetBytes( + String.Join("\n", + zipFile.Cast() + .Where(e => !e.IsUnicodeText) + .Select(e => e.Name) + ) + ); + var detectionResult = CharsetDetector.DetectFromBytes(fileNameBytes); + if (detectionResult.Detected != null && detectionResult.Detected.Confidence > 0.5) + { + return detectionResult.Detected.Encoding; + } + else + { + return null; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"SharpZipLib error: {ex.Message}"); + return null; + } + } + /// 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 c829bb40622f..dd7e302ef424 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -2099,6 +2099,9 @@ Encoding + + (detected) + Path diff --git a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs index b1ff52bc2b35..f96ba27bac86 100644 --- a/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs +++ b/src/Files.App/ViewModels/Dialogs/DecompressArchiveDialogViewModel.cs @@ -44,6 +44,16 @@ public bool IsArchiveEncodingUndetermined set => SetProperty(ref isArchiveEncodingUndetermined, value); } + private Encoding? detectedEncoding; + public Encoding? DetectedEncoding + { + get => detectedEncoding; + set { + SetProperty(ref detectedEncoding, value); + RefreshEncodingOptions(); + } + } + private bool showPathSelection; public bool ShowPathSelection { @@ -53,19 +63,27 @@ 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[] EncodingOptions { get; set; } = EncodingItem.Defaults; public EncodingItem SelectedEncoding { get; set; } + void RefreshEncodingOptions() + { + if (detectedEncoding != null) + { + EncodingOptions = EncodingItem.Defaults + .Prepend(new EncodingItem( + detectedEncoding, + detectedEncoding.EncodingName + Strings.EncodingDetected.GetLocalizedResource()) + ) + .ToArray(); + } + else + { + EncodingOptions = EncodingItem.Defaults; + } + SelectedEncoding = EncodingOptions.FirstOrDefault(); + } + + public IRelayCommand PrimaryButtonClickCommand { get; private set; }