1.11.0
This commit is contained in:
parent
9df59f2e13
commit
161bc06148
|
@ -106,6 +106,11 @@ typedef struct _FILE_DRIVE FILE_DRIVE;
|
|||
|
||||
static ULONG File_FindBoxPrefixLength(const WCHAR* CopyPath);
|
||||
|
||||
NTSTATUS File_GetCopyPath(WCHAR *TruePath, WCHAR **OutCopyPath);
|
||||
|
||||
NTSTATUS File_GetTruePath(WCHAR *CopyPath, WCHAR **OutTruePath);
|
||||
|
||||
WCHAR* File_FindSnapshotPath(WCHAR* CopyPath);
|
||||
|
||||
SBIEDLL_EXPORT NTSTATUS File_GetName(
|
||||
HANDLE RootDirectory, UNICODE_STRING *ObjectName,
|
||||
|
@ -318,6 +323,21 @@ static const ULONG File_MupLen = 12;
|
|||
|
||||
const WCHAR *File_BQQB = L"\\??\\";
|
||||
|
||||
static const ULONG _DeviceLen = 8;
|
||||
static const WCHAR *_Share = L"\\share\\";
|
||||
static const ULONG _ShareLen = 7;
|
||||
static const WCHAR *_Drive = L"\\drive\\";
|
||||
static const ULONG _DriveLen = 7;
|
||||
|
||||
static const WCHAR *_User = L"\\user";
|
||||
static const ULONG _UserLen = 5;
|
||||
static const WCHAR *_UserAll = L"\\user\\all";
|
||||
static const ULONG _UserAllLen = 9;
|
||||
static const WCHAR *_UserCurrent = L"\\user\\current";
|
||||
static const ULONG _UserCurrentLen = 13;
|
||||
static const WCHAR *_UserPublic = L"\\user\\public";
|
||||
static const ULONG _UserPublicLen = 12;
|
||||
|
||||
#ifdef WOW64_FS_REDIR
|
||||
static WCHAR *File_Wow64System32 = NULL;
|
||||
static ULONG File_Wow64System32Len = 0;
|
||||
|
@ -384,6 +404,467 @@ _FX ULONG File_FindBoxPrefixLength(const WCHAR* CopyPath)
|
|||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// File_GetCopyPathImpl
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FX NTSTATUS File_GetCopyPathImpl(WCHAR* TruePath, WCHAR **OutCopyPath, ULONG *OutFlags, WCHAR* snapshot_id, BOOLEAN have_trailing_backslash, BOOLEAN* p_add_trailing_backslash)
|
||||
{
|
||||
THREAD_DATA *TlsData = Dll_GetTlsData(NULL);
|
||||
|
||||
ULONG length;
|
||||
WCHAR* name;
|
||||
const FILE_DRIVE *drive;
|
||||
ULONG PrefixLength;
|
||||
|
||||
length = wcslen(TruePath);
|
||||
name = Dll_GetTlsNameBuffer(
|
||||
TlsData, COPY_NAME_BUFFER, Dll_BoxFilePathLen + length);
|
||||
|
||||
*OutCopyPath = name;
|
||||
|
||||
wmemcpy(name, Dll_BoxFilePath, Dll_BoxFilePathLen);
|
||||
name += Dll_BoxFilePathLen;
|
||||
|
||||
//
|
||||
// if we requested real paths, re add the snapshot prefix
|
||||
//
|
||||
|
||||
if (snapshot_id && *snapshot_id) {
|
||||
|
||||
*name++ = L'\\';
|
||||
wmemcpy(name, File_Snapshot_Prefix, File_Snapshot_PrefixLen);
|
||||
name += File_Snapshot_PrefixLen;
|
||||
ULONG len = wcslen(snapshot_id);
|
||||
wmemcpy(name, snapshot_id, len);
|
||||
name += len;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// if the true path points to a remote share or mapped drive,
|
||||
// convert that to box the portable form "\share\computer\folder"
|
||||
//
|
||||
|
||||
PrefixLength = 0;
|
||||
if (length >= File_RedirectorLen && _wcsnicmp(TruePath, File_Redirector, File_RedirectorLen) == 0)
|
||||
PrefixLength = File_RedirectorLen;
|
||||
else if (length >= File_DfsClientRedirLen && _wcsnicmp(TruePath, File_DfsClientRedir, File_DfsClientRedirLen) == 0)
|
||||
PrefixLength = File_DfsClientRedirLen;
|
||||
else if (length >= File_HgfsRedirLen && _wcsnicmp(TruePath, File_HgfsRedir, File_HgfsRedirLen) == 0)
|
||||
PrefixLength = File_HgfsRedirLen;
|
||||
else if (length >= File_MupRedirLen && _wcsnicmp(TruePath, File_MupRedir, File_MupRedirLen) == 0)
|
||||
PrefixLength = File_MupRedirLen;
|
||||
|
||||
if (PrefixLength) {
|
||||
|
||||
WCHAR *ptr = TruePath + PrefixLength;
|
||||
if (*ptr == L';') {
|
||||
ptr = wcschr(ptr, L'\\');
|
||||
if (! ptr)
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
++ptr;
|
||||
}
|
||||
|
||||
wmemcpy(name, _Share, _ShareLen);
|
||||
name += _ShareLen;
|
||||
|
||||
length = wcslen(ptr);
|
||||
wmemcpy(name, ptr, length);
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_NETWORK_SHARE;
|
||||
|
||||
// does this next section really need to be different than above?
|
||||
} else if (length >= File_MupLen &&
|
||||
_wcsnicmp(TruePath, File_Mup, File_MupLen) == 0) {
|
||||
|
||||
WCHAR *ptr = TruePath + File_MupLen;
|
||||
if (*ptr == L';') // like \Device\Mup\;RdpDr;:2\...
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
ptr = wcschr(ptr, L'\\');
|
||||
if (File_IsPipeSuffix(ptr))
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
|
||||
wmemcpy(name, _Share, _ShareLen);
|
||||
name += _ShareLen;
|
||||
|
||||
length -= File_MupLen;
|
||||
wmemcpy(name, TruePath + File_MupLen, length);
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_NETWORK_SHARE;
|
||||
}
|
||||
|
||||
//
|
||||
// if the true path begins with the full path to the home folder
|
||||
// for the AllUsers or for the current user, then we translate
|
||||
// the copy path to the box portable form "\user\all" or
|
||||
// "\user\current", respectively
|
||||
//
|
||||
|
||||
else if (File_AllUsersLen && length >= File_AllUsersLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_AllUsers, File_AllUsersLen))
|
||||
{
|
||||
wmemcpy(name, _UserAll, _UserAllLen);
|
||||
name += _UserAllLen;
|
||||
|
||||
length -= File_AllUsersLen;
|
||||
wmemcpy(name, TruePath + File_AllUsersLen, length);
|
||||
|
||||
}
|
||||
|
||||
else if (File_CurrentUserLen && length >= File_CurrentUserLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_CurrentUser, File_CurrentUserLen))
|
||||
{
|
||||
wmemcpy(name, _UserCurrent, _UserCurrentLen);
|
||||
name += _UserCurrentLen;
|
||||
|
||||
length -= File_CurrentUserLen;
|
||||
wmemcpy(name, TruePath + File_CurrentUserLen, length);
|
||||
|
||||
}
|
||||
|
||||
else if (File_PublicUserLen && length >= File_PublicUserLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_PublicUser, File_PublicUserLen))
|
||||
{
|
||||
wmemcpy(name, _UserPublic, _UserPublicLen);
|
||||
name += _UserPublicLen;
|
||||
|
||||
length -= File_PublicUserLen;
|
||||
wmemcpy(name, TruePath + File_PublicUserLen, length);
|
||||
}
|
||||
|
||||
//
|
||||
// otherwise, if the true path begins with the NT path for one of
|
||||
// the known DosDevices drives, then translate to the box portable
|
||||
// form "\drive\x"
|
||||
//
|
||||
|
||||
else {
|
||||
|
||||
ULONG drive_len;
|
||||
|
||||
drive = File_GetDriveForPath(TruePath, length);
|
||||
if (drive)
|
||||
drive_len = drive->len;
|
||||
else
|
||||
drive = File_GetDriveForUncPath(TruePath, length, &drive_len);
|
||||
|
||||
if (drive) {
|
||||
|
||||
WCHAR drive_letter = drive->letter;
|
||||
|
||||
LeaveCriticalSection(File_DrivesAndLinks_CritSec);
|
||||
|
||||
wmemcpy(name, _Drive, _DriveLen);
|
||||
name += _DriveLen;
|
||||
*name = drive_letter;
|
||||
++name;
|
||||
|
||||
if (File_DriveAddSN && *drive->sn) {
|
||||
|
||||
*name = L'~';
|
||||
++name;
|
||||
wcscpy(name, drive->sn);
|
||||
name += 9;
|
||||
}
|
||||
|
||||
*name = L'\0';
|
||||
|
||||
if (length == drive_len) {
|
||||
|
||||
//
|
||||
// in the special case of a request to open the
|
||||
// volume device itself, rather than any file within
|
||||
// the device, we return a special status code
|
||||
//
|
||||
|
||||
if (! have_trailing_backslash)
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
|
||||
//
|
||||
// otherwise, caller must want to open the root
|
||||
// directory of the device, so remember to add the
|
||||
// trailing backslash before we're done
|
||||
//
|
||||
|
||||
if (p_add_trailing_backslash) *p_add_trailing_backslash = TRUE;
|
||||
}
|
||||
|
||||
length -= drive_len;
|
||||
wmemcpy(name, TruePath + drive_len, length);
|
||||
|
||||
} else {
|
||||
|
||||
//
|
||||
// if we couldn't find any matching logical drive, then
|
||||
// we return STATUS_BAD_INITIAL_PC so this DLL does not
|
||||
// try any further sandboxing. (But the driver will still
|
||||
// block any attempt to access disk devices.)
|
||||
//
|
||||
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// null-terminate the copy path, and add the missing trailing
|
||||
// backslash to the true path, if there was one
|
||||
//
|
||||
|
||||
name += length;
|
||||
*name = L'\0';
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// File_GetCopyPath
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FX NTSTATUS File_GetCopyPath(WCHAR* TruePath, WCHAR **OutCopyPath)
|
||||
{
|
||||
return File_GetCopyPathImpl(TruePath, OutCopyPath, NULL, NULL, FALSE, NULL);
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// File_GetTruePathImpl
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FX NTSTATUS File_GetTruePathImpl(ULONG* p_length, WCHAR **OutTruePath, ULONG *OutFlags, BOOLEAN* p_is_boxed_path, BOOLEAN no_relocation, WCHAR* snapshot_id, BOOLEAN* p_convert_links_again)
|
||||
{
|
||||
THREAD_DATA *TlsData = Dll_GetTlsData(NULL);
|
||||
|
||||
WCHAR* name;
|
||||
const FILE_DRIVE *drive;
|
||||
|
||||
check_sandbox_prefix:
|
||||
|
||||
if (*p_length >= Dll_BoxFilePathLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
*OutTruePath, Dll_BoxFilePath, Dll_BoxFilePathLen))
|
||||
{
|
||||
*OutTruePath += Dll_BoxFilePathLen;
|
||||
*p_length -= Dll_BoxFilePathLen;
|
||||
|
||||
if (! *p_length) {
|
||||
//
|
||||
// caller specified just the sandbox prefix
|
||||
//
|
||||
*OutTruePath = NULL;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_IS_BOXED_PATH;
|
||||
*p_is_boxed_path = TRUE;
|
||||
}
|
||||
|
||||
if (File_AltBoxPath &&
|
||||
*p_length >= File_AltBoxPathLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
*OutTruePath, File_AltBoxPath, File_AltBoxPathLen))
|
||||
{
|
||||
*OutTruePath += File_AltBoxPathLen;
|
||||
*p_length -= File_AltBoxPathLen;
|
||||
|
||||
if (! *p_length) {
|
||||
//
|
||||
// caller specified just the sandbox prefix
|
||||
//
|
||||
*OutTruePath = NULL;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_IS_BOXED_PATH;
|
||||
*p_is_boxed_path = TRUE;
|
||||
}
|
||||
|
||||
//
|
||||
// If its a sandboxed file, check if its in the current image or in a snapshot
|
||||
// If its in a snapshot remove the snapshot prefix
|
||||
//
|
||||
|
||||
if (p_is_boxed_path) {
|
||||
if (*p_length >= 10 && 0 == Dll_NlsStrCmp(*OutTruePath + 1, File_Snapshot_Prefix, File_Snapshot_PrefixLen))
|
||||
{
|
||||
WCHAR* path = wcschr(*OutTruePath + 1 + File_Snapshot_PrefixLen, L'\\');
|
||||
if (path == NULL) {
|
||||
//
|
||||
// caller specified just the sandbox snapshot prefix, or the path is to long
|
||||
//
|
||||
*OutTruePath = NULL;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (no_relocation) {
|
||||
ULONG len = (ULONG)(path - (*OutTruePath + 1 + File_Snapshot_PrefixLen));
|
||||
if (len < FILE_MAX_SNAPSHOT_ID) {
|
||||
wmemcpy(snapshot_id, *OutTruePath + 1 + File_Snapshot_PrefixLen, len);
|
||||
snapshot_id[len] = L'\0';
|
||||
}
|
||||
}
|
||||
|
||||
*p_length -= (ULONG)(path - *OutTruePath);
|
||||
*OutTruePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// the true path may now begin with "\drive\x", for instance,
|
||||
// if the process specified a RootDirectory handle that leads
|
||||
// inside the box. we have to change this box convention to
|
||||
// full NT path of the drive letter. a later section of code
|
||||
// will change it back to \drive\x for the copy path.
|
||||
//
|
||||
// note that we temporarily use the COPY_NAME_BUFFER here, but
|
||||
// that's ok because it hasn't been initialized yet
|
||||
//
|
||||
|
||||
if (*p_length >= (_DriveLen - 1) &&
|
||||
_wcsnicmp(*OutTruePath, _Drive, _DriveLen - 1) == 0)
|
||||
{
|
||||
name = (*OutTruePath);
|
||||
if (name[_DriveLen - 1] == L'\\')
|
||||
drive = File_GetDriveForLetter(name[_DriveLen]);
|
||||
else
|
||||
drive = NULL;
|
||||
|
||||
if (! drive) {
|
||||
//
|
||||
// caller specified invalid path for \sandbox\drive\x
|
||||
//
|
||||
*OutTruePath = NULL;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
ULONG len = _DriveLen + 1; /* drive letter */
|
||||
|
||||
// skip any suffix after the drive letter
|
||||
if (File_DriveAddSN) {
|
||||
WCHAR* ptr = wcschr(*OutTruePath + _DriveLen + 1, L'\\');
|
||||
if (!ptr) ptr = wcschr(*OutTruePath + _DriveLen + 1, L'\0');
|
||||
len = (ULONG)(ptr - *OutTruePath);
|
||||
}
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, p_length, len,
|
||||
drive->path, drive->len);
|
||||
|
||||
if (p_convert_links_again) *p_convert_links_again = TRUE;
|
||||
|
||||
LeaveCriticalSection(File_DrivesAndLinks_CritSec);
|
||||
|
||||
goto check_sandbox_prefix;
|
||||
}
|
||||
|
||||
//
|
||||
// alternatively, the true path may begin with "\user\all" which,
|
||||
// is a box convention for the AllUsers home folder. or, with
|
||||
// "\user\current", which is a box convention for the home folder
|
||||
// of the current user. both cases must be translated similarly
|
||||
// to the "\drive\x" case above.
|
||||
//
|
||||
// note that we temporarily use the COPY_NAME_BUFFER here, but
|
||||
// that's ok because it hasn't been initialized yet
|
||||
//
|
||||
|
||||
else if (*p_length >= _UserLen &&
|
||||
_wcsnicmp(*OutTruePath, _User, _UserLen) == 0) {
|
||||
|
||||
if (File_AllUsersLen && *p_length >= _UserAllLen &&
|
||||
_wcsnicmp(*OutTruePath, _UserAll, _UserAllLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, p_length, _UserAllLen,
|
||||
File_AllUsers, File_AllUsersLen);
|
||||
|
||||
} else if (File_CurrentUserLen &&
|
||||
*p_length >= _UserCurrentLen && _wcsnicmp(
|
||||
*OutTruePath, _UserCurrent, _UserCurrentLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, p_length, _UserCurrentLen,
|
||||
File_CurrentUser, File_CurrentUserLen);
|
||||
|
||||
} else if (File_PublicUserLen &&
|
||||
*p_length >= _UserPublicLen && _wcsnicmp(
|
||||
*OutTruePath, _UserPublic, _UserPublicLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, p_length, _UserPublicLen,
|
||||
File_PublicUser, File_PublicUserLen);
|
||||
|
||||
} else {
|
||||
|
||||
//
|
||||
// the path is "...\box\user..." but not for user\current or
|
||||
// user\all, so restore the sandbox prefix and return special
|
||||
// status for read-only access
|
||||
//
|
||||
|
||||
name = Dll_GetTlsNameBuffer(
|
||||
TlsData, TRUE_NAME_BUFFER,
|
||||
(Dll_BoxFilePathLen + *p_length + 1) * sizeof(WCHAR));
|
||||
|
||||
wmemmove(name + Dll_BoxFilePathLen, *OutTruePath, *p_length + 1);
|
||||
wmemcpy(name, Dll_BoxFilePath, Dll_BoxFilePathLen);
|
||||
|
||||
*OutTruePath = name;
|
||||
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (p_convert_links_again) *p_convert_links_again = TRUE;
|
||||
}
|
||||
|
||||
//
|
||||
// alternatively, the true path may begin with "\share\..." which,
|
||||
// is a box convention for remote shares. in this case it has to
|
||||
// be translated similarly to the "\drive\x" case above.
|
||||
//
|
||||
|
||||
else if (*p_length >= _ShareLen &&
|
||||
_wcsnicmp(*OutTruePath, _Share, _ShareLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, p_length, _ShareLen,
|
||||
File_Mup, File_MupLen);
|
||||
|
||||
if (p_convert_links_again) *p_convert_links_again = TRUE;
|
||||
}
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// File_GetTruePath
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FX NTSTATUS File_GetTruePath(WCHAR *CopyPath, WCHAR **OutTruePath)
|
||||
{
|
||||
ULONG length = wcslen(CopyPath);
|
||||
BOOLEAN is_boxed_path = FALSE;
|
||||
*OutTruePath = CopyPath;
|
||||
NTSTATUS status = File_GetTruePathImpl(&length, OutTruePath, NULL, &is_boxed_path, FALSE, NULL, NULL);
|
||||
if (NT_SUCCESS(status) && !is_boxed_path)
|
||||
return STATUS_BAD_INITIAL_STACK; // indicate with this error code that the path provided was already the true path
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// File_GetName
|
||||
//---------------------------------------------------------------------------
|
||||
|
@ -392,22 +873,7 @@ _FX ULONG File_FindBoxPrefixLength(const WCHAR* CopyPath)
|
|||
_FX NTSTATUS File_GetName(
|
||||
HANDLE RootDirectory, UNICODE_STRING *ObjectName,
|
||||
WCHAR **OutTruePath, WCHAR **OutCopyPath, ULONG *OutFlags)
|
||||
{
|
||||
static const ULONG _DeviceLen = 8;
|
||||
static const WCHAR *_Share = L"\\share\\";
|
||||
static const ULONG _ShareLen = 7;
|
||||
static const WCHAR *_Drive = L"\\drive\\";
|
||||
static const ULONG _DriveLen = 7;
|
||||
|
||||
static const WCHAR *_User = L"\\user";
|
||||
static const ULONG _UserLen = 5;
|
||||
static const WCHAR *_UserAll = L"\\user\\all";
|
||||
static const ULONG _UserAllLen = 9;
|
||||
static const WCHAR *_UserCurrent = L"\\user\\current";
|
||||
static const ULONG _UserCurrentLen = 13;
|
||||
static const WCHAR *_UserPublic = L"\\user\\public";
|
||||
static const ULONG _UserPublicLen = 12;
|
||||
|
||||
{
|
||||
THREAD_DATA *TlsData = Dll_GetTlsData(NULL);
|
||||
|
||||
NTSTATUS status;
|
||||
|
@ -421,7 +887,6 @@ _FX NTSTATUS File_GetName(
|
|||
BOOLEAN convert_links_again;
|
||||
BOOLEAN is_boxed_path;
|
||||
BOOLEAN free_true_path;
|
||||
ULONG PrefixLength;
|
||||
|
||||
#ifdef WOW64_FS_REDIR
|
||||
BOOLEAN convert_wow64_link = (File_Wow64FileLink) ? TRUE : FALSE;
|
||||
|
@ -767,202 +1232,11 @@ _FX NTSTATUS File_GetName(
|
|||
|
||||
check_sandbox_prefix:
|
||||
|
||||
if (length >= Dll_BoxFilePathLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
*OutTruePath, Dll_BoxFilePath, Dll_BoxFilePathLen))
|
||||
{
|
||||
*OutTruePath += Dll_BoxFilePathLen;
|
||||
length -= Dll_BoxFilePathLen;
|
||||
|
||||
if (! length) {
|
||||
//
|
||||
// caller specified just the sandbox prefix
|
||||
//
|
||||
status = File_GetTruePathImpl(&length, OutTruePath, OutFlags, &is_boxed_path, no_relocation, snapshot_id, &convert_links_again);
|
||||
if (!NT_SUCCESS(status)) {
|
||||
if(*OutTruePath == NULL)
|
||||
*OutTruePath = TruePath;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_IS_BOXED_PATH;
|
||||
is_boxed_path = TRUE;
|
||||
}
|
||||
|
||||
if (File_AltBoxPath &&
|
||||
length >= File_AltBoxPathLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
*OutTruePath, File_AltBoxPath, File_AltBoxPathLen))
|
||||
{
|
||||
*OutTruePath += File_AltBoxPathLen;
|
||||
length -= File_AltBoxPathLen;
|
||||
|
||||
if (! length) {
|
||||
//
|
||||
// caller specified just the sandbox prefix
|
||||
//
|
||||
*OutTruePath = TruePath;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_IS_BOXED_PATH;
|
||||
is_boxed_path = TRUE;
|
||||
}
|
||||
|
||||
//
|
||||
// If its a sandboxed file, check if its in the current image or in a snapshot
|
||||
// If its in a snapshot remove the snapshot prefix
|
||||
//
|
||||
|
||||
if (is_boxed_path) {
|
||||
if (length >= 10 && 0 == Dll_NlsStrCmp(*OutTruePath + 1, File_Snapshot_Prefix, File_Snapshot_PrefixLen))
|
||||
{
|
||||
WCHAR* path = wcschr(*OutTruePath + 1 + File_Snapshot_PrefixLen, L'\\');
|
||||
if (path == NULL) {
|
||||
|
||||
//
|
||||
// caller specified just the sandbox snapshot prefix, or the path is to long
|
||||
//
|
||||
|
||||
*OutTruePath = TruePath;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
if (no_relocation) {
|
||||
ULONG len = (ULONG)(path - (*OutTruePath + 1 + File_Snapshot_PrefixLen));
|
||||
if (len < FILE_MAX_SNAPSHOT_ID) {
|
||||
wmemcpy(snapshot_id, *OutTruePath + 1 + File_Snapshot_PrefixLen, len);
|
||||
snapshot_id[len] = L'\0';
|
||||
}
|
||||
}
|
||||
|
||||
length -= (ULONG)(path - *OutTruePath);
|
||||
*OutTruePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// the true path may now begin with "\drive\x", for instance,
|
||||
// if the process specified a RootDirectory handle that leads
|
||||
// inside the box. we have to change this box convention to
|
||||
// full NT path of the drive letter. a later section of code
|
||||
// will change it back to \drive\x for the copy path.
|
||||
//
|
||||
// note that we temporarily use the COPY_NAME_BUFFER here, but
|
||||
// that's ok because it hasn't been initialized yet
|
||||
//
|
||||
|
||||
if (length >= (_DriveLen - 1) &&
|
||||
_wcsnicmp(*OutTruePath, _Drive, _DriveLen - 1) == 0)
|
||||
{
|
||||
name = (*OutTruePath);
|
||||
if (name[_DriveLen - 1] == L'\\')
|
||||
drive = File_GetDriveForLetter(name[_DriveLen]);
|
||||
else
|
||||
drive = NULL;
|
||||
|
||||
if (! drive) {
|
||||
//
|
||||
// caller specified invalid path for \sandbox\drive\x
|
||||
//
|
||||
*OutTruePath = TruePath;
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
ULONG len = _DriveLen + 1; /* drive letter */
|
||||
|
||||
// skip any suffix after the drive letter
|
||||
if (File_DriveAddSN) {
|
||||
WCHAR* ptr = wcschr(*OutTruePath + _DriveLen + 1, L'\\');
|
||||
if (!ptr) ptr = wcschr(*OutTruePath + _DriveLen + 1, L'\0');
|
||||
len = (ULONG)(ptr - *OutTruePath);
|
||||
}
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, &length, len,
|
||||
drive->path, drive->len);
|
||||
|
||||
convert_links_again = TRUE;
|
||||
|
||||
LeaveCriticalSection(File_DrivesAndLinks_CritSec);
|
||||
|
||||
goto check_sandbox_prefix;
|
||||
}
|
||||
|
||||
//
|
||||
// alternatively, the true path may begin with "\user\all" which,
|
||||
// is a box convention for the AllUsers home folder. or, with
|
||||
// "\user\current", which is a box convention for the home folder
|
||||
// of the current user. both cases must be translated similarly
|
||||
// to the "\drive\x" case above.
|
||||
//
|
||||
// note that we temporarily use the COPY_NAME_BUFFER here, but
|
||||
// that's ok because it hasn't been initialized yet
|
||||
//
|
||||
|
||||
else if (//SbieApi_QueryConfBool(NULL, L"SeparateUserFolders", TRUE) && // if we disable File_InitUsers we don't need to do it here and below
|
||||
length >= _UserLen &&
|
||||
_wcsnicmp(*OutTruePath, _User, _UserLen) == 0) {
|
||||
|
||||
if (File_AllUsersLen && length >= _UserAllLen &&
|
||||
_wcsnicmp(*OutTruePath, _UserAll, _UserAllLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, &length, _UserAllLen,
|
||||
File_AllUsers, File_AllUsersLen);
|
||||
|
||||
} else if (File_CurrentUserLen &&
|
||||
length >= _UserCurrentLen && _wcsnicmp(
|
||||
*OutTruePath, _UserCurrent, _UserCurrentLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, &length, _UserCurrentLen,
|
||||
File_CurrentUser, File_CurrentUserLen);
|
||||
|
||||
} else if (File_PublicUserLen &&
|
||||
length >= _UserPublicLen && _wcsnicmp(
|
||||
*OutTruePath, _UserPublic, _UserPublicLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, &length, _UserPublicLen,
|
||||
File_PublicUser, File_PublicUserLen);
|
||||
|
||||
} else {
|
||||
|
||||
//
|
||||
// the path is "...\box\user..." but not for user\current or
|
||||
// user\all, so restore the sandbox prefix and return special
|
||||
// status for read-only access
|
||||
//
|
||||
|
||||
name = Dll_GetTlsNameBuffer(
|
||||
TlsData, TRUE_NAME_BUFFER,
|
||||
(Dll_BoxFilePathLen + length + 1) * sizeof(WCHAR));
|
||||
|
||||
wmemmove(name + Dll_BoxFilePathLen, *OutTruePath, length + 1);
|
||||
wmemcpy(name, Dll_BoxFilePath, Dll_BoxFilePathLen);
|
||||
|
||||
*OutTruePath = name;
|
||||
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
|
||||
convert_links_again = TRUE;
|
||||
}
|
||||
|
||||
//
|
||||
// alternatively, the true path may begin with "\share\..." which,
|
||||
// is a box convention for remote shares. in this case it has to
|
||||
// be translated similarly to the "\drive\x" case above.
|
||||
//
|
||||
|
||||
else if (length >= _ShareLen &&
|
||||
_wcsnicmp(*OutTruePath, _Share, _ShareLen) == 0) {
|
||||
|
||||
File_GetName_FixTruePrefix(TlsData,
|
||||
OutTruePath, &length, _ShareLen,
|
||||
File_Mup, File_MupLen);
|
||||
|
||||
convert_links_again = TRUE;
|
||||
return status;
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -1109,209 +1383,9 @@ check_sandbox_prefix:
|
|||
// still be missing its null terminator.
|
||||
//
|
||||
|
||||
name = Dll_GetTlsNameBuffer(
|
||||
TlsData, COPY_NAME_BUFFER, Dll_BoxFilePathLen + length);
|
||||
|
||||
*OutCopyPath = name;
|
||||
|
||||
wmemcpy(name, Dll_BoxFilePath, Dll_BoxFilePathLen);
|
||||
name += Dll_BoxFilePathLen;
|
||||
|
||||
//
|
||||
// if we requested real paths, re add the snapshot prefix
|
||||
//
|
||||
|
||||
if (*snapshot_id) {
|
||||
|
||||
*name++ = L'\\';
|
||||
wmemcpy(name, File_Snapshot_Prefix, File_Snapshot_PrefixLen);
|
||||
name += File_Snapshot_PrefixLen;
|
||||
ULONG len = wcslen(snapshot_id);
|
||||
wmemcpy(name, snapshot_id, len);
|
||||
name += len;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// if the true path points to a remote share or mapped drive,
|
||||
// convert that to box the portable form "\share\computer\folder"
|
||||
//
|
||||
|
||||
PrefixLength = 0;
|
||||
if (length >= File_RedirectorLen && _wcsnicmp(TruePath, File_Redirector, File_RedirectorLen) == 0)
|
||||
PrefixLength = File_RedirectorLen;
|
||||
else if (length >= File_DfsClientRedirLen && _wcsnicmp(TruePath, File_DfsClientRedir, File_DfsClientRedirLen) == 0)
|
||||
PrefixLength = File_DfsClientRedirLen;
|
||||
else if (length >= File_HgfsRedirLen && _wcsnicmp(TruePath, File_HgfsRedir, File_HgfsRedirLen) == 0)
|
||||
PrefixLength = File_HgfsRedirLen;
|
||||
else if (length >= File_MupRedirLen && _wcsnicmp(TruePath, File_MupRedir, File_MupRedirLen) == 0)
|
||||
PrefixLength = File_MupRedirLen;
|
||||
|
||||
if (PrefixLength) {
|
||||
|
||||
WCHAR *ptr = TruePath + PrefixLength;
|
||||
if (*ptr == L';') {
|
||||
ptr = wcschr(ptr, L'\\');
|
||||
if (! ptr)
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
++ptr;
|
||||
}
|
||||
|
||||
wmemcpy(name, _Share, _ShareLen);
|
||||
name += _ShareLen;
|
||||
|
||||
length = wcslen(ptr);
|
||||
wmemcpy(name, ptr, length);
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_NETWORK_SHARE;
|
||||
|
||||
// does this next section really need to be different than above?
|
||||
} else if (length >= File_MupLen &&
|
||||
_wcsnicmp(TruePath, File_Mup, File_MupLen) == 0) {
|
||||
|
||||
WCHAR *ptr = TruePath + File_MupLen;
|
||||
if (*ptr == L';') // like \Device\Mup\;RdpDr;:2\...
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
ptr = wcschr(ptr, L'\\');
|
||||
if (File_IsPipeSuffix(ptr))
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
|
||||
wmemcpy(name, _Share, _ShareLen);
|
||||
name += _ShareLen;
|
||||
|
||||
length -= File_MupLen;
|
||||
wmemcpy(name, TruePath + File_MupLen, length);
|
||||
|
||||
if (OutFlags)
|
||||
*OutFlags |= FGN_NETWORK_SHARE;
|
||||
}
|
||||
|
||||
//
|
||||
// if the true path begins with the full path to the home folder
|
||||
// for the AllUsers or for the current user, then we translate
|
||||
// the copy path to the box portable form "\user\all" or
|
||||
// "\user\current", respectively
|
||||
//
|
||||
|
||||
else if (//SbieApi_QueryConfBool(NULL, L"SeparateUserFolders", TRUE) &&
|
||||
File_AllUsersLen && length >= File_AllUsersLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_AllUsers, File_AllUsersLen))
|
||||
{
|
||||
wmemcpy(name, _UserAll, _UserAllLen);
|
||||
name += _UserAllLen;
|
||||
|
||||
length -= File_AllUsersLen;
|
||||
wmemcpy(name, TruePath + File_AllUsersLen, length);
|
||||
|
||||
}
|
||||
|
||||
else if (//SbieApi_QueryConfBool(NULL, L"SeparateUserFolders", TRUE) &&
|
||||
File_CurrentUserLen && length >= File_CurrentUserLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_CurrentUser, File_CurrentUserLen))
|
||||
{
|
||||
wmemcpy(name, _UserCurrent, _UserCurrentLen);
|
||||
name += _UserCurrentLen;
|
||||
|
||||
length -= File_CurrentUserLen;
|
||||
wmemcpy(name, TruePath + File_CurrentUserLen, length);
|
||||
|
||||
}
|
||||
|
||||
else if (//SbieApi_QueryConfBool(NULL, L"SeparateUserFolders", TRUE) &&
|
||||
File_PublicUserLen && length >= File_PublicUserLen &&
|
||||
0 == Dll_NlsStrCmp(
|
||||
TruePath, File_PublicUser, File_PublicUserLen))
|
||||
{
|
||||
wmemcpy(name, _UserPublic, _UserPublicLen);
|
||||
name += _UserPublicLen;
|
||||
|
||||
length -= File_PublicUserLen;
|
||||
wmemcpy(name, TruePath + File_PublicUserLen, length);
|
||||
}
|
||||
|
||||
//
|
||||
// otherwise, if the true path begins with the NT path for one of
|
||||
// the known DosDevices drives, then translate to the box portable
|
||||
// form "\drive\x"
|
||||
//
|
||||
|
||||
else {
|
||||
|
||||
ULONG drive_len;
|
||||
|
||||
drive = File_GetDriveForPath(TruePath, length);
|
||||
if (drive)
|
||||
drive_len = drive->len;
|
||||
else
|
||||
drive = File_GetDriveForUncPath(TruePath, length, &drive_len);
|
||||
|
||||
if (drive) {
|
||||
|
||||
WCHAR drive_letter = drive->letter;
|
||||
|
||||
LeaveCriticalSection(File_DrivesAndLinks_CritSec);
|
||||
|
||||
wmemcpy(name, _Drive, _DriveLen);
|
||||
name += _DriveLen;
|
||||
*name = drive_letter;
|
||||
++name;
|
||||
|
||||
if (File_DriveAddSN && *drive->sn) {
|
||||
|
||||
*name = L'~';
|
||||
++name;
|
||||
wcscpy(name, drive->sn);
|
||||
name += 9;
|
||||
}
|
||||
|
||||
*name = L'\0';
|
||||
|
||||
if (length == drive_len) {
|
||||
|
||||
//
|
||||
// in the special case of a request to open the
|
||||
// volume device itself, rather than any file within
|
||||
// the device, we return a special status code
|
||||
//
|
||||
|
||||
if (! have_trailing_backslash)
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
|
||||
//
|
||||
// otherwise, caller must want to open the root
|
||||
// directory of the device, so remember to add the
|
||||
// trailing backslash before we're done
|
||||
//
|
||||
|
||||
add_trailing_backslash = TRUE;
|
||||
}
|
||||
|
||||
length -= drive_len;
|
||||
wmemcpy(name, TruePath + drive_len, length);
|
||||
|
||||
} else {
|
||||
|
||||
//
|
||||
// if we couldn't find any matching logical drive, then
|
||||
// we return STATUS_BAD_INITIAL_PC so this DLL does not
|
||||
// try any further sandboxing. (But the driver will still
|
||||
// block any attempt to access disk devices.)
|
||||
//
|
||||
|
||||
return STATUS_BAD_INITIAL_PC;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// null-terminate the copy path, and add the missing trailing
|
||||
// backslash to the true path, if there was one
|
||||
//
|
||||
|
||||
name += length;
|
||||
*name = L'\0';
|
||||
status = File_GetCopyPathImpl(TruePath, OutCopyPath, OutFlags, snapshot_id, have_trailing_backslash, &add_trailing_backslash);
|
||||
if (!NT_SUCCESS(status))
|
||||
return status;
|
||||
|
||||
if (add_trailing_backslash) {
|
||||
name = *OutTruePath;
|
||||
|
|
|
@ -166,7 +166,7 @@ _FX BOOLEAN File_Init(void)
|
|||
return FALSE;
|
||||
}
|
||||
|
||||
if (Dll_OsBuild >= 6000) { // needed for File_GetFileName used indirectly by File_InitRecoverFolders
|
||||
if (Dll_OsBuild >= 6000) {
|
||||
|
||||
void *GetFinalPathNameByHandleW =
|
||||
GetProcAddress(Dll_KernelBase ? Dll_KernelBase : Dll_Kernel32,
|
||||
|
|
|
@ -831,34 +831,142 @@ _FX FILE_LINK *File_AddTempLink(WCHAR *path)
|
|||
|
||||
P_NtCreateFile pNtCreateFile = __sys_NtCreateFile;
|
||||
P_NtClose pNtClose = __sys_NtClose;
|
||||
if (! pNtCreateFile)
|
||||
P_NtFsControlFile pNtFsControlFile = __sys_NtFsControlFile;
|
||||
if (!pNtCreateFile) {
|
||||
SbieApi_Log(2325, L"File_AddTempLink !pNtCreateFile");
|
||||
pNtCreateFile = NtCreateFile;
|
||||
}
|
||||
if (! pNtClose)
|
||||
pNtClose = NtClose;
|
||||
if (! pNtFsControlFile)
|
||||
pNtFsControlFile = NtFsControlFile;
|
||||
|
||||
stop = TRUE;
|
||||
|
||||
InitializeObjectAttributes(
|
||||
&objattrs, &objname, OBJ_CASE_INSENSITIVE, NULL, NULL);
|
||||
|
||||
RtlInitUnicodeString(&objname, path);
|
||||
BOOLEAN UserReparse = SbieApi_QueryConfBool(NULL, L"UseNewSymlinkResolver", FALSE);
|
||||
|
||||
status = pNtCreateFile(
|
||||
&handle, FILE_READ_ATTRIBUTES | SYNCHRONIZE, &objattrs,
|
||||
&IoStatusBlock, NULL, 0, FILE_SHARE_VALID_FLAGS,
|
||||
FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
|
||||
NULL, 0);
|
||||
if (UserReparse) {
|
||||
|
||||
//
|
||||
// first try the copy path
|
||||
//
|
||||
|
||||
if (_wcsnicmp(path, Dll_BoxFilePath, Dll_BoxFilePathLen) != 0)
|
||||
{
|
||||
THREAD_DATA* TlsData = Dll_GetTlsData(NULL);
|
||||
|
||||
WCHAR* CopyPath = NULL;
|
||||
|
||||
|
||||
Dll_PushTlsNameBuffer(TlsData);
|
||||
|
||||
File_GetCopyPath(path, &CopyPath);
|
||||
|
||||
//
|
||||
// get tempalte file if present, and reparese the path
|
||||
//
|
||||
|
||||
WCHAR* TmplName = File_FindSnapshotPath(CopyPath);
|
||||
if (TmplName != NULL)
|
||||
RtlInitUnicodeString(&objname, TmplName);
|
||||
else
|
||||
RtlInitUnicodeString(&objname, CopyPath);
|
||||
|
||||
status = pNtCreateFile(
|
||||
&handle, FILE_GENERIC_READ | SYNCHRONIZE, &objattrs,
|
||||
&IoStatusBlock, NULL, 0, FILE_SHARE_VALID_FLAGS,
|
||||
FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
|
||||
NULL, 0);
|
||||
|
||||
Dll_PopTlsNameBuffer(TlsData);
|
||||
}
|
||||
else
|
||||
status = STATUS_BAD_INITIAL_PC;
|
||||
|
||||
//
|
||||
// then try the true path
|
||||
//
|
||||
|
||||
if (!NT_SUCCESS(status)) {
|
||||
|
||||
RtlInitUnicodeString(&objname, path);
|
||||
|
||||
status = pNtCreateFile(
|
||||
&handle, FILE_GENERIC_READ | SYNCHRONIZE, &objattrs,
|
||||
&IoStatusBlock, NULL, 0, FILE_SHARE_VALID_FLAGS,
|
||||
FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
|
||||
NULL, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
RtlInitUnicodeString(&objname, path);
|
||||
|
||||
status = pNtCreateFile(
|
||||
&handle, FILE_READ_ATTRIBUTES | SYNCHRONIZE, &objattrs,
|
||||
&IoStatusBlock, NULL, 0, FILE_SHARE_VALID_FLAGS,
|
||||
FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
|
||||
NULL, 0);
|
||||
}
|
||||
|
||||
if (NT_SUCCESS(status)) {
|
||||
|
||||
if (UserReparse) {
|
||||
|
||||
REPARSE_DATA_BUFFER* reparseDataBuffer = Dll_AllocTemp(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
|
||||
status = pNtFsControlFile(handle, NULL, NULL, NULL, &IoStatusBlock, FSCTL_GET_REPARSE_POINT, NULL, 0, reparseDataBuffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
|
||||
|
||||
if (NT_SUCCESS(status)) {
|
||||
|
||||
WCHAR* input_str = reparseDataBuffer->MountPointReparseBuffer.PathBuffer;
|
||||
if (_wcsnicmp(input_str, File_BQQB, 4) == 0)
|
||||
input_str = File_TranslateDosToNtPath(reparseDataBuffer->MountPointReparseBuffer.PathBuffer + 4);
|
||||
|
||||
newpath = File_TranslateTempLinks_2(input_str, wcslen(input_str));
|
||||
|
||||
if (input_str != reparseDataBuffer->MountPointReparseBuffer.PathBuffer)
|
||||
Dll_Free(input_str);
|
||||
|
||||
/*THREAD_DATA* TlsData = Dll_GetTlsData(NULL);
|
||||
|
||||
Dll_PushTlsNameBuffer(TlsData);
|
||||
|
||||
WCHAR* TruePath = NULL;
|
||||
if (NT_SUCCESS(File_GetTruePath(newpath, &TruePath))) {
|
||||
|
||||
Dll_Free(newpath);
|
||||
newpath = Dll_AllocTemp((wcslen(TruePath) + 1) * sizeof(WCHAR));
|
||||
wcscpy(newpath, TruePath);
|
||||
}
|
||||
|
||||
Dll_PopTlsNameBuffer(TlsData);*/
|
||||
}
|
||||
else //if (status == STATUS_NOT_A_REPARSE_POINT)
|
||||
{
|
||||
newpath = Dll_AllocTemp((wcslen(path) + 1) * sizeof(WCHAR));
|
||||
wcscpy(newpath, path);
|
||||
status = STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
Dll_Free(reparseDataBuffer);
|
||||
}
|
||||
|
||||
//
|
||||
// get the reparsed absolute path
|
||||
//
|
||||
|
||||
const ULONG PATH_BUF_LEN = 1024;
|
||||
newpath = Dll_AllocTemp(PATH_BUF_LEN);
|
||||
|
||||
status = File_GetFileName(handle, PATH_BUF_LEN - 4, newpath);
|
||||
if (!UserReparse) {
|
||||
|
||||
newpath = Dll_AllocTemp(PATH_BUF_LEN);
|
||||
|
||||
status = File_GetFileName(handle, PATH_BUF_LEN - 4, newpath);
|
||||
}
|
||||
|
||||
if (NT_SUCCESS(status)) {
|
||||
|
||||
//
|
||||
|
|
Loading…
Reference in New Issue