Skip to content

Commit c64184f

Browse files
authored
Add support for alternate streams (#9234)
1 parent 800b083 commit c64184f

File tree

15 files changed

+333
-17
lines changed

15 files changed

+333
-17
lines changed

src/Files.Backend/Services/Settings/IPreferencesSettingsService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public interface IPreferencesSettingsService : IBaseSettingsService, INotifyProp
3535
/// </summary>
3636
bool AreSystemItemsHidden { get; set; }
3737

38+
/// <summary>
39+
/// Gets or sets a value indicating whether or not alternate data streams should be visible.
40+
/// </summary>
41+
bool AreAlternateStreamsVisible { get; set; }
42+
3843
/// <summary>
3944
/// Gets or sets a value indicating whether or not to display dot files.
4045
/// </summary>

src/Files.FullTrust/MessageHandlers/ApplicationLaunchHandler.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,31 @@ private async Task<bool> HandleApplicationLaunch(string application, Dictionary<
175175
});
176176
}
177177
}
178+
if (!opened)
179+
{
180+
var isAlternateStream = Regex.IsMatch(application, @"\w:\w");
181+
if (isAlternateStream)
182+
{
183+
var basePath = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Guid.NewGuid().ToString("n"));
184+
Kernel32.CreateDirectory(basePath);
185+
186+
var tempPath = Path.Combine(basePath, new string(Path.GetFileName(application).SkipWhile(x => x != ':').Skip(1).ToArray()));
187+
using var hFileSrc = Kernel32.CreateFile(application, Kernel32.FileAccess.GENERIC_READ, FileShare.ReadWrite, null, FileMode.Open, FileFlagsAndAttributes.FILE_ATTRIBUTE_NORMAL);
188+
using var hFileDst = Kernel32.CreateFile(tempPath, Kernel32.FileAccess.GENERIC_WRITE, 0, null, FileMode.Create, FileFlagsAndAttributes.FILE_ATTRIBUTE_NORMAL | FileFlagsAndAttributes.FILE_ATTRIBUTE_READONLY);
189+
190+
if (!hFileSrc.IsInvalid && !hFileDst.IsInvalid)
191+
{
192+
// Copy ADS to temp folder and open
193+
using (var inStream = new FileStream(hFileSrc.DangerousGetHandle(), FileAccess.Read))
194+
using (var outStream = new FileStream(hFileDst.DangerousGetHandle(), FileAccess.Write))
195+
{
196+
await inStream.CopyToAsync(outStream);
197+
await outStream.FlushAsync();
198+
}
199+
opened = await HandleApplicationLaunch(tempPath, message);
200+
}
201+
}
202+
}
178203
return opened;
179204
}
180205
catch (Win32Exception)
@@ -194,6 +219,12 @@ private async Task<bool> HandleApplicationLaunch(string application, Dictionary<
194219
// Invalid file path
195220
return false;
196221
}
222+
catch (Exception ex)
223+
{
224+
// Generic error, log
225+
Program.Logger.Warn(ex, $"Error launching: {application}");
226+
return false;
227+
}
197228
}
198229

199230
private string GetMtpPath(string executable)

src/Files.FullTrust/Win32API.cs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ public static (string icon, string overlay) GetFileIconAndOverlay(string path, i
169169

170170
if (!onlyGetOverlay)
171171
{
172-
using var shellItem = ShellFolderExtensions.GetShellItemFromPathOrPidl(path);
173-
if (shellItem.IShellItem is Shell32.IShellItemImageFactory fctry)
172+
using var shellItem = SafetyExtensions.IgnoreExceptions(() => ShellFolderExtensions.GetShellItemFromPathOrPidl(path));
173+
if (shellItem != null && shellItem.IShellItem is Shell32.IShellItemImageFactory fctry)
174174
{
175175
var flags = Shell32.SIIGBF.SIIGBF_BIGGERSIZEOK;
176176
if (thumbnailSize < 80) flags |= Shell32.SIIGBF.SIIGBF_ICONONLY;
@@ -188,13 +188,15 @@ public static (string icon, string overlay) GetFileIconAndOverlay(string path, i
188188
}
189189
}
190190

191-
if (getOverlay)
191+
if (getOverlay || (!onlyGetOverlay && iconStr == null))
192192
{
193193
var shfi = new Shell32.SHFILEINFO();
194194
var flags = Shell32.SHGFI.SHGFI_OVERLAYINDEX | Shell32.SHGFI.SHGFI_ICON | Shell32.SHGFI.SHGFI_SYSICONINDEX | Shell32.SHGFI.SHGFI_ICONLOCATION;
195+
var useFileAttibutes = !onlyGetOverlay && iconStr == null; // Cannot access file, use file attributes
195196
var ret = ShellFolderExtensions.GetStringAsPidl(path, out var pidl) ?
196197
Shell32.SHGetFileInfo(pidl, 0, ref shfi, Shell32.SHFILEINFO.Size, Shell32.SHGFI.SHGFI_PIDL | flags) :
197-
Shell32.SHGetFileInfo(path, 0, ref shfi, Shell32.SHFILEINFO.Size, flags);
198+
// TODO: pass FileAttributes.Directory for folders (add "isFolder" parameter)
199+
Shell32.SHGetFileInfo(path, 0, ref shfi, Shell32.SHFILEINFO.Size, flags | (useFileAttibutes ? Shell32.SHGFI.SHGFI_USEFILEATTRIBUTES : 0));
198200
if (ret == IntPtr.Zero)
199201
{
200202
return (iconStr, null);
@@ -204,13 +206,46 @@ public static (string icon, string overlay) GetFileIconAndOverlay(string path, i
204206

205207
lock (lockObject)
206208
{
207-
if (!Shell32.SHGetImageList(Shell32.SHIL.SHIL_LARGE, typeof(ComCtl32.IImageList).GUID, out var imageList).Succeeded)
209+
var imageListSize = thumbnailSize switch
210+
{
211+
<= 16 => Shell32.SHIL.SHIL_SMALL,
212+
<= 32 => Shell32.SHIL.SHIL_LARGE,
213+
<= 48 => Shell32.SHIL.SHIL_EXTRALARGE,
214+
_ => Shell32.SHIL.SHIL_JUMBO,
215+
};
216+
if (!Shell32.SHGetImageList(imageListSize, typeof(ComCtl32.IImageList).GUID, out var imageList).Succeeded)
208217
{
209218
return (iconStr, null);
210219
}
211220

221+
if (!onlyGetOverlay && iconStr == null)
222+
{
223+
var iconIdx = shfi.iIcon & 0xFFFFFF;
224+
if (iconIdx != 0)
225+
{
226+
// Could not fetch thumbnail, load simple icon
227+
using var hIcon = imageList.GetIcon(iconIdx, ComCtl32.IMAGELISTDRAWFLAGS.ILD_TRANSPARENT);
228+
if (!hIcon.IsNull && !hIcon.IsInvalid)
229+
{
230+
using (var icon = hIcon.ToIcon())
231+
using (var image = icon.ToBitmap())
232+
{
233+
byte[] bitmapData = (byte[])new ImageConverter().ConvertTo(image, typeof(byte[]));
234+
iconStr = Convert.ToBase64String(bitmapData, 0, bitmapData.Length);
235+
}
236+
}
237+
}
238+
else
239+
{
240+
// Could not icon, load generic icon
241+
var icons = ExtractSelectedIconsFromDLL(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "shell32.dll"), new[] { 1 }, thumbnailSize);
242+
var generic = icons.SingleOrDefault(x => x.Index == 1);
243+
iconStr = generic?.IconData;
244+
}
245+
}
246+
212247
var overlayIdx = shfi.iIcon >> 24;
213-
if (overlayIdx != 0)
248+
if (overlayIdx != 0 && getOverlay)
214249
{
215250
var overlayImage = imageList.GetOverlayImage(overlayIdx);
216251
using var hOverlay = imageList.GetIcon(overlayImage, ComCtl32.IMAGELISTDRAWFLAGS.ILD_TRANSPARENT);

src/Files.Uwp/Filesystem/BaseStorage/BaseStorageFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public IAsyncOperation<StorageItemThumbnail> GetScaledImageAsThumbnailAsync(Thum
9090

9191
public static IAsyncOperation<BaseStorageFile> GetFileFromPathAsync(string path)
9292
=> AsyncInfo.Run(async (cancellationToken)
93-
=> await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await ShellStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path)
93+
=> await ZipStorageFile.FromPathAsync(path) ?? await FtpStorageFile.FromPathAsync(path) ?? await ShellStorageFile.FromPathAsync(path) ?? await NativeStorageFile.FromPathAsync(path) ?? await SystemStorageFile.FromPathAsync(path)
9494
);
9595

9696
public async Task<string> ReadTextAsync(int maxLength = -1)

src/Files.Uwp/Filesystem/FilesystemOperations/Helpers/FilesystemHelpers.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,19 @@ public class FilesystemHelpers : IFilesystemHelpers
4242

4343
#region Helpers Members
4444

45-
private static readonly char[] RestrictedCharacters = new[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' };
45+
private static char[] RestrictedCharacters
46+
{
47+
get
48+
{
49+
var userSettingsService = Ioc.Default.GetService<IUserSettingsService>();
50+
if (userSettingsService.PreferencesSettingsService.AreAlternateStreamsVisible)
51+
{
52+
// Allow ":" char
53+
return new[] { '\\', '/', '*', '?', '"', '<', '>', '|' };
54+
}
55+
return new[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' };
56+
}
57+
}
4658

4759
private static readonly string[] RestrictedFileNames = new string[]
4860
{
@@ -782,7 +794,7 @@ public async Task<ReturnResult> RecycleItemsFromClipboard(DataPackageView packag
782794

783795
public static bool HasDraggedStorageItems(DataPackageView packageView)
784796
{
785-
return packageView != null && (packageView.Contains(StandardDataFormats.StorageItems) || (packageView.Properties.TryGetValue("FileDrop", out var data)));
797+
return packageView != null && (packageView.Contains(StandardDataFormats.StorageItems) || (packageView.Properties.TryGetValue("FileDrop", out _)));
786798
}
787799

788800
public static async Task<bool> CheckDragNeedsFulltrust(DataPackageView packageView)

src/Files.Uwp/Filesystem/FilesystemOperations/ShellFilesystemOperations.cs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,17 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
196196
{
197197
await DialogDisplayHelper.ShowDialogAsync("ItemAlreadyExistsDialogTitle".GetLocalized(), "ItemAlreadyExistsDialogContent".GetLocalized());
198198
}
199+
else if (copyResult.Items.All(x => x.HResult == -1)) // ADS
200+
{
201+
// Retry with StorageFile API
202+
var failedSources = copyResult.Items.Where(x => !x.Succeeded);
203+
var copyZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
204+
var sourceMatch = await failedSources.Select(x => copyZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x != null).ToListAsync();
205+
return await filesystemOperations.CopyItemsAsync(
206+
await sourceMatch.Select(x => x.src).ToListAsync(),
207+
await sourceMatch.Select(x => x.dest).ToListAsync(),
208+
await sourceMatch.Select(x => x.coll).ToListAsync(), progress, errorCode, cancellationToken);
209+
}
199210
errorCode?.Report(CopyEngineResult.Convert(copyResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
200211
return null;
201212
}
@@ -448,14 +459,18 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
448459
else if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong))
449460
{
450461
// Abort, path is too long for recycle bin
451-
//var failedSources = deleteResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong);
452-
//var sourceMatch = await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x != null).ToListAsync();
453-
//return await filesystemOperations.DeleteItemsAsync(sourceMatch, progress, errorCode, permanently, cancellationToken);
454462
}
455463
else if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NotFound))
456464
{
457465
await DialogDisplayHelper.ShowDialogAsync("FileNotFoundDialog/Title".GetLocalized(), "FileNotFoundDialog/Text".GetLocalized());
458466
}
467+
else if (deleteResult.Items.All(x => x.HResult == -1) && permanently) // ADS
468+
{
469+
// Retry with StorageFile API
470+
var failedSources = deleteResult.Items.Where(x => !x.Succeeded);
471+
var sourceMatch = await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x != null).ToListAsync();
472+
return await filesystemOperations.DeleteItemsAsync(sourceMatch, progress, errorCode, permanently, cancellationToken);
473+
}
459474
errorCode?.Report(CopyEngineResult.Convert(deleteResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
460475
return null;
461476
}
@@ -615,6 +630,17 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
615630
{
616631
await DialogDisplayHelper.ShowDialogAsync("ItemAlreadyExistsDialogTitle".GetLocalized(), "ItemAlreadyExistsDialogContent".GetLocalized());
617632
}
633+
else if (moveResult.Items.All(x => x.HResult == -1)) // ADS
634+
{
635+
// Retry with StorageFile API
636+
var failedSources = moveResult.Items.Where(x => !x.Succeeded);
637+
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
638+
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x != null).ToListAsync();
639+
return await filesystemOperations.MoveItemsAsync(
640+
await sourceMatch.Select(x => x.src).ToListAsync(),
641+
await sourceMatch.Select(x => x.dest).ToListAsync(),
642+
await sourceMatch.Select(x => x.coll).ToListAsync(), progress, errorCode, cancellationToken);
643+
}
618644
errorCode?.Report(CopyEngineResult.Convert(moveResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
619645
return null;
620646
}
@@ -696,6 +722,11 @@ public async Task<IStorageHistory> RenameAsync(IStorageItemWithPath source, stri
696722
{
697723
await DialogDisplayHelper.ShowDialogAsync("ItemAlreadyExistsDialogTitle".GetLocalized(), "ItemAlreadyExistsDialogContent".GetLocalized());
698724
}
725+
else if (renameResult.Items.All(x => x.HResult == -1)) // ADS
726+
{
727+
// Retry with StorageFile API
728+
return await filesystemOperations.RenameAsync(source, newName, collision, errorCode, cancellationToken);
729+
}
699730
errorCode?.Report(CopyEngineResult.Convert(renameResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
700731
return null;
701732
}

src/Files.Uwp/Filesystem/ListedItem.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ public ObservableCollection<FileProperty> ItemProperties
365365
public ListedItem(string folderRelativeId) => FolderRelativeId = folderRelativeId;
366366

367367
// Parameterless constructor for JsonConvert
368-
public ListedItem() {}
368+
public ListedItem() { }
369369

370370
private ObservableCollection<FileProperty> fileDetails;
371371
public ObservableCollection<FileProperty> FileDetails
@@ -403,6 +403,7 @@ public override string ToString()
403403
public bool IsLinkItem => IsShortcutItem && ((ShortcutItem)this).IsUrl;
404404
public bool IsFtpItem => this is FtpItem;
405405
public bool IsZipItem => this is ZipItem;
406+
public bool IsAlternateStreamItem => this is AlternateStreamItem;
406407
public virtual bool IsExecutable => new[] { ".exe", ".bat", ".cmd" }.Contains(Path.GetExtension(ItemPath), StringComparer.OrdinalIgnoreCase);
407408
public bool IsPinned => App.SidebarPinnedController.Model.FavoriteItems.Contains(itemPath);
408409

@@ -585,4 +586,24 @@ public LibraryItem(LibraryLocationItem lib) : base(null)
585586

586587
public ReadOnlyCollection<string> Folders { get; }
587588
}
589+
590+
public class AlternateStreamItem : ListedItem
591+
{
592+
public string MainStreamPath => ItemPath.Substring(0, ItemPath.LastIndexOf(":"));
593+
public string MainStreamName => Path.GetFileName(MainStreamPath);
594+
595+
public override string ItemName
596+
{
597+
get
598+
{
599+
var nameWithoutExtension = Path.GetFileNameWithoutExtension(ItemNameRaw);
600+
var mainStreamNameWithoutExtension = Path.GetFileNameWithoutExtension(MainStreamName);
601+
if (!UserSettingsService.PreferencesSettingsService.ShowFileExtensions)
602+
{
603+
return $"{(string.IsNullOrEmpty(mainStreamNameWithoutExtension) ? MainStreamName : mainStreamNameWithoutExtension)}:{(string.IsNullOrEmpty(nameWithoutExtension) ? ItemNameRaw : nameWithoutExtension)}";
604+
}
605+
return $"{MainStreamName}:{ItemNameRaw}";
606+
}
607+
}
608+
}
588609
}

0 commit comments

Comments
 (0)