1
0
mirror of https://github.com/winscp/winscp.git synced 2026-10-24 02:28:55 +03:00

Not scanning local directory symlinks pointing to network drives

Source commit: 19baa413392caabbb7afea6eb2b925cc98ca5076
This commit is contained in:
Martin Prikryl
2026-10-06 12:29:32 +00:00
parent a9fa4a583a
commit 0b3eca59ad
4 changed files with 113 additions and 9 deletions

View File

@@ -3,9 +3,16 @@
#pragma hdrstop
#include <Common.h>
#include <System.DateUtils.hpp>
#include <System.IOUtils.hpp>
#include "CoreMain.h"
#include "DriveView.h"
#include "IEDriveInfo.h"
#include "DirView.h"
#ifdef DESIGN_ONLY
#undef AppLogFmt
#define AppLogFmt(S, F)
#endif
//---------------------------------------------------------------------------
#pragma package(smart_init)
//---------------------------------------------------------------------------
@@ -73,10 +80,16 @@ void __fastcall TDriveView::CreateWnd()
void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node)
{
auto NodeData = static_cast<TNodeData *>(Node->Data);
TDateTime Start = Now();
UnicodeString Path = NodePath(Node);
TSearchRec SRec;
if (!FindFirstSubDir(IncludeTrailingBackslash(Path) + L"*.*", SRec))
{
int Sec = static_cast<int>(SecondsBetween(Now(), Start));
if (Sec > 2)
{
AppLogFmt(L"Took %d s to find that '%s' cannot be read", (Sec, Path));
}
Node->HasChildren = false;
}
else
@@ -88,7 +101,7 @@ void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node)
Limit /= 10;
CheckInterval /= 10;
}
if (!ReadSubDirsBatch(Node, SRec, CheckInterval, Limit))
if (!ReadSubDirsBatch(Node, SRec, CheckInterval, Limit, Start))
{
NodeData->DelayedSrec = SRec;
NodeData->DelayedExclude = new TStringList();
@@ -108,7 +121,8 @@ void __fastcall TDriveView::ReadSubDirs(TTreeNode * Node)
//---------------------------------------------------------------------------
bool __fastcall TDriveView::DoScanDir(TTreeNode * FromNode)
{
return !static_cast<TNodeData *>(FromNode->Data)->IsRecycleBin;
auto NodeData = static_cast<TNodeData *>(FromNode->Data);
return !NodeData->IsRecycleBin && !NodeData->IsRemote;
}
//---------------------------------------------------------------------------
void __fastcall TDriveView::AddChildNode(TTreeNode * ParentNode, UnicodeString ParentPath, const TSearchRec & SRec)
@@ -116,19 +130,32 @@ void __fastcall TDriveView::AddChildNode(TTreeNode * ParentNode, UnicodeString P
auto NodeData = new TNodeData();
NodeData->Attr = SRec.Attr;
NodeData->DirName = SRec.Name;
UnicodeString Path = IncludeTrailingBackslash(ParentPath) + SRec.Name;
NodeData->IsRecycleBin =
FLAGSET(SRec.Attr, faSysFile) &&
(ParentNode->Parent == nullptr) &&
(SameText(SRec.Name, L"RECYCLED") ||
SameText(SRec.Name, L"RECYCLER") ||
SameText(SRec.Name, L"$RECYCLE.BIN"));
#ifndef DESIGN_ONLY
if (FLAGSET(SRec.Attr, faSymLink))
{
String Target;
NodeData->IsRemote =
FileGetSymLinkTargetSafe(Path, Target) &&
TPath::IsUNCRooted(Target);
}
#endif
NodeData->Scanned = false;
TTreeNode * NewNode = Items->AddChildObject(ParentNode, EmptyStr, NodeData);
NewNode->Text = GetDisplayName(NewNode);
NewNode->HasChildren = true;
if (GetDriveTypetoNode(ParentNode) != DRIVE_REMOTE)
if ((GetDriveTypetoNode(ParentNode) != DRIVE_REMOTE) &&
!NodeData->IsRemote)
{
FSubDirReaderThread->Add(NewNode, IncludeTrailingBackslash(ParentPath) + SRec.Name);
FSubDirReaderThread->Add(NewNode, Path);
}
}

View File

@@ -8,6 +8,7 @@
#include <shlwapi.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <winioctl.h>
#include <SessionInfo.h>
#include <Soap.EncdDecd.hpp>
#pragma clang diagnostic push
@@ -1057,6 +1058,7 @@ bool __fastcall IsReservedName(UnicodeString FileName)
// Inspired by
// https://stackoverflow.com/q/18580945/850848
// This can be reimplemented using PathCchCanonicalizeEx on Windows 8 and later
// There's also TPath.GetPosAfterExtendedPrefix.
enum PATH_PREFIX_TYPE
{
PPT_UNKNOWN,
@@ -1589,6 +1591,73 @@ void __fastcall ProcessLocalDirectory(UnicodeString DirName,
}
}
//---------------------------------------------------------------------------
// Similar structure might be a part of WINAPI eventually (VCLCOPY - sic)
struct REPARSE_DATA_BUFFER
{
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union
{
struct
{
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} SymbolicLink;
struct
{
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPoint;
} ReparseBuffer;
};
//---------------------------------------------------------------------------
// Contrary to VCL's FileGetSymLinkTarget, this does not try to open the target folder and does not throw an exception.
// Also it would call CreateFile without FILE_FLAG_OPEN_REPARSE_POINT, what also tries to access the remote directory,
// potentially stalling. And with FILE_FLAG_OPEN_REPARSE_POINT, the GetFinalPathNameByHandle does not work.
bool FileGetSymLinkTargetSafe(const String & FileName, String & Target)
{
bool Result = false;
HANDLE Handle =
CreateFile(
FileName.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
if (Handle != INVALID_HANDLE_VALUE)
{
DWORD bytesReturned = 0;
char Buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
auto Reparse = reinterpret_cast<REPARSE_DATA_BUFFER *>(Buffer);
if (DeviceIoControl(Handle, FSCTL_GET_REPARSE_POINT, NULL, 0, Reparse, sizeof(Buffer), &bytesReturned, NULL))
{
BYTE * NamePtr = nullptr;
USHORT NameSize = 0;
if (Reparse->ReparseTag == IO_REPARSE_TAG_SYMLINK)
{
NamePtr = reinterpret_cast<BYTE*>(Reparse->ReparseBuffer.SymbolicLink.PathBuffer) + Reparse->ReparseBuffer.SymbolicLink.PrintNameOffset;
NameSize = Reparse->ReparseBuffer.SymbolicLink.PrintNameLength;
}
else if (Reparse->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
{
NamePtr = reinterpret_cast<BYTE*>(Reparse->ReparseBuffer.MountPoint.PathBuffer) + Reparse->ReparseBuffer.MountPoint.PrintNameOffset;
NameSize = Reparse->ReparseBuffer.MountPoint.PrintNameLength;
}
if (NamePtr != nullptr)
{
Target = String(reinterpret_cast<wchar_t *>(NamePtr), NameSize / sizeof(wchar_t));
Result = true;
}
}
}
return Result;
}
//---------------------------------------------------------------------------
int __fastcall FileGetAttrFix(const UnicodeString & FileName)
{
// Already called with ApiPath
@@ -1610,6 +1679,11 @@ int __fastcall FileGetAttrFix(const UnicodeString & FileName)
// On Samba, InternalGetFileNameFromSymLink fails and returns true but empty target.
// That confuses FileGetAttr, which returns attributes of the parent folder instead.
// Using FileGetSymLinkTarget solves the problem, as it returns false.
// Might want to use FileGetSymLinkTargetSafe here for consistency,
// but flaws/limitations of VCL FileGetSymLinkTarget probably does not matter here.
// The FileGetSymLinkTarget does not return the target when it cannot open it.
// But then GetFileAttributes would fail anyway too, so we return -1 is either case.
if (!FileGetSymLinkTarget(FileName, TargetName))
{
// FileGetAttr would return faInvalid (-1), but we want to allow an upload from Samba,

View File

@@ -269,6 +269,7 @@ int __fastcall FindNextChecked(TSearchRecChecked & F);
int __fastcall FindNextUnchecked(TSearchRecChecked & F);
void __fastcall ProcessLocalDirectory(UnicodeString DirName,
TProcessLocalFileEvent CallBackFunc, void * Param = NULL, int FindAttrs = -1);
bool FileGetSymLinkTargetSafe(const UnicodeString & FileName, UnicodeString & Target);
int __fastcall FileGetAttrFix(const UnicodeString & FileName);
//---------------------------------------------------------------------------
extern const wchar_t * DSTModeNames;

View File

@@ -80,6 +80,7 @@ type
FScanned: Boolean;
FData: Pointer;
FIsRecycleBin: Boolean;
FIsRemote: Boolean;
FIconEmpty: Boolean;
FSchedule: TSubDirReaderSchedule;
@@ -95,6 +96,7 @@ type
property Scanned: Boolean read FScanned write FScanned;
property Data: Pointer read FData write FData;
property IsRecycleBin: Boolean read FIsRecycleBin write FIsRecycleBin;
property IsRemote: Boolean read FIsRemote write FIsRemote;
property IconEmpty: Boolean read FIconEmpty write FIconEmpty;
property Schedule: TSubDirReaderSchedule read FSchedule write FSchedule;
end;
@@ -183,7 +185,7 @@ type
procedure ReadSubDirs(Node: TTreeNode); virtual; abstract;
procedure CancelDelayedNode(Node: TTreeNode);
procedure DelayedNodeTimer(Sender: TObject);
function ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer): Boolean;
function ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer; Start: TDateTime): Boolean;
procedure UpdateDelayedNodeTimer;
function DoSearchSubDirs(
ParentNode: TTreeNode; Path: string; Level: Integer; ExistingOnly: Boolean;
@@ -436,6 +438,7 @@ begin
FScanned := False;
FDirName := '';
FIsRecycleBin := False;
FIsRemote := False;
FIconEmpty := True;
FSchedule := nil;
DelayedExclude := nil;
@@ -785,6 +788,7 @@ begin
NewData.Scanned := SourceData.Scanned;
NewData.Data := SourceData.Data;
NewData.FIsRecycleBin := SourceData.FIsRecycleBin;
NewData.FIsRemote := SourceData.FIsRemote;
NewData.IconEmpty := SourceData.IconEmpty;
TTreeNode(Source).Data := NewData;
end;
@@ -1853,15 +1857,13 @@ begin
Result := (FindNext(SRec) = 0) and GetSubDir(SRec);
end;
function TDriveViewInt.ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer): Boolean;
function TDriveViewInt.ReadSubDirsBatch(Node: TTreeNode; var SRec: TSearchRec; CheckInterval, Limit: Integer; Start: TDateTime): Boolean;
var
Start: TDateTime;
Cont: Boolean;
Path: string;
Count: Integer;
DelayedExclude: TStringList;
begin
Start := Now;
Path := NodePath(Node);
Result := True;
Count := 0;
@@ -1916,7 +1918,7 @@ begin
else
begin
NodeData := TNodeData(Node.Data);
if ReadSubDirsBatch(Node, NodeData.DelayedSrec, 10, 50) then
if ReadSubDirsBatch(Node, NodeData.DelayedSrec, 10, 50, Now) then
begin
FreeAndNil(NodeData.DelayedExclude);
FDelayedNodes.Delete(0);