mirror of
https://github.com/reactos/reactos.git
synced 2025-08-03 13:45:50 +00:00
[BOOTLIB]:
- Fix multiple bugs in ETFS code (confusion between file offset vs. disk offset) - Implement EtfsGetInformation, EtfsSetInformation, and fix ETFS_FILE definition to make this easy. - Implement EtfsRead. - Fix multiple bugs in file I/O code (swapped/reversed validation checks) - Make BlStatusPrint call EfiPrintf on debug builds, even without BD. - Add some additional error logging. svn path=/trunk/; revision=69452
This commit is contained in:
parent
10dbbf573b
commit
0cc3cfadf3
6 changed files with 170 additions and 29 deletions
|
@ -49,7 +49,7 @@ typedef struct _BL_PACKED_BOOT_ERROR
|
||||||
ULONG Size;
|
ULONG Size;
|
||||||
} BL_PACKED_BOOT_ERROR, *PBL_PACKED_BOOT_ERROR;
|
} BL_PACKED_BOOT_ERROR, *PBL_PACKED_BOOT_ERROR;
|
||||||
|
|
||||||
#define BL_FATAL_ERROR_BCD_READ 0x02
|
#define BL_FATAL_ERROR_BCD_READ 0x01
|
||||||
|
|
||||||
/* FUNCTIONS *****************************************************************/
|
/* FUNCTIONS *****************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -321,7 +321,7 @@ NTSTATUS
|
||||||
_In_ struct _BL_FILE_ENTRY* FileEntry,
|
_In_ struct _BL_FILE_ENTRY* FileEntry,
|
||||||
_In_ PVOID Buffer,
|
_In_ PVOID Buffer,
|
||||||
_In_ ULONG Size,
|
_In_ ULONG Size,
|
||||||
_Out_ PULONG BytesRead
|
_Out_opt_ PULONG BytesRead
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef
|
typedef
|
||||||
|
@ -786,8 +786,10 @@ typedef struct _BL_ADDRESS_RANGE
|
||||||
|
|
||||||
typedef struct _BL_FILE_INFORMATION
|
typedef struct _BL_FILE_INFORMATION
|
||||||
{
|
{
|
||||||
ULONGLONG FileSize;
|
ULONGLONG Size;
|
||||||
ULONGLONG CurrentOffset;
|
ULONGLONG Offset;
|
||||||
|
PWCHAR FsName;
|
||||||
|
ULONG Flags;
|
||||||
} BL_FILE_INFORMATION, *PBL_FILE_INFORMATION;
|
} BL_FILE_INFORMATION, *PBL_FILE_INFORMATION;
|
||||||
|
|
||||||
typedef struct _BL_FILE_CALLBACKS
|
typedef struct _BL_FILE_CALLBACKS
|
||||||
|
@ -809,7 +811,7 @@ typedef struct _BL_FILE_ENTRY
|
||||||
ULONG Flags;
|
ULONG Flags;
|
||||||
ULONG ReferenceCount;
|
ULONG ReferenceCount;
|
||||||
ULONG Unknown;
|
ULONG Unknown;
|
||||||
ULONGLONG Unknown1;
|
ULONGLONG TotalBytesRead;
|
||||||
ULONGLONG Unknown2;
|
ULONGLONG Unknown2;
|
||||||
BL_FILE_CALLBACKS Callbacks;
|
BL_FILE_CALLBACKS Callbacks;
|
||||||
PVOID FsSpecificData;
|
PVOID FsSpecificData;
|
||||||
|
|
|
@ -36,12 +36,12 @@ typedef struct _BL_ETFS_DEVICE
|
||||||
|
|
||||||
typedef struct _BL_ETFS_FILE
|
typedef struct _BL_ETFS_FILE
|
||||||
{
|
{
|
||||||
|
ULONG DiskOffset;
|
||||||
ULONG DirOffset;
|
ULONG DirOffset;
|
||||||
ULONG DirEntOffset;
|
ULONG DirEntOffset;
|
||||||
ULONGLONG Size;
|
|
||||||
ULONGLONG Offset;
|
BL_FILE_INFORMATION;
|
||||||
PWCHAR FsName;
|
|
||||||
ULONG Flags;
|
|
||||||
ULONG DeviceId;
|
ULONG DeviceId;
|
||||||
} BL_ETFS_FILE, *PBL_ETFS_FILE;
|
} BL_ETFS_FILE, *PBL_ETFS_FILE;
|
||||||
|
|
||||||
|
@ -56,9 +56,35 @@ EtfsOpen (
|
||||||
_Out_ PBL_FILE_ENTRY *FileEntry
|
_Out_ PBL_FILE_ENTRY *FileEntry
|
||||||
);
|
);
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsGetInformation (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_Out_ PBL_FILE_INFORMATION FileInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsSetInformation (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_In_ PBL_FILE_INFORMATION FileInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsRead (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_In_ PVOID Buffer,
|
||||||
|
_In_ ULONG Size,
|
||||||
|
_Out_opt_ PULONG BytesReturned
|
||||||
|
);
|
||||||
|
|
||||||
BL_FILE_CALLBACKS EtfsFunctionTable =
|
BL_FILE_CALLBACKS EtfsFunctionTable =
|
||||||
{
|
{
|
||||||
EtfsOpen,
|
EtfsOpen,
|
||||||
|
NULL,
|
||||||
|
EtfsRead,
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
EtfsGetInformation,
|
||||||
|
EtfsSetInformation
|
||||||
};
|
};
|
||||||
|
|
||||||
/* FUNCTIONS *****************************************************************/
|
/* FUNCTIONS *****************************************************************/
|
||||||
|
@ -205,7 +231,7 @@ EtfspGetDirent (
|
||||||
|
|
||||||
EtfsFile = DirectoryEntry->FsSpecificData;
|
EtfsFile = DirectoryEntry->FsSpecificData;
|
||||||
DeviceId = EtfsFile->DeviceId;
|
DeviceId = EtfsFile->DeviceId;
|
||||||
FileOffset = EtfsFile->Offset;
|
FileOffset = EtfsFile->DiskOffset;
|
||||||
EtfsDevice = EtfsDeviceTable[DeviceId];
|
EtfsDevice = EtfsDeviceTable[DeviceId];
|
||||||
|
|
||||||
DirectoryOffset = *DirentOffset;
|
DirectoryOffset = *DirentOffset;
|
||||||
|
@ -362,7 +388,7 @@ EtfspCachedSearchForDirent (
|
||||||
DirentOffset = EtfsFile->DirEntOffset;
|
DirentOffset = EtfsFile->DirEntOffset;
|
||||||
|
|
||||||
if ((KeepOffset) ||
|
if ((KeepOffset) ||
|
||||||
(ALIGN_DOWN_BY((DirentOffset + EtfsFile->Offset), CD_SECTOR_SIZE) ==
|
(ALIGN_DOWN_BY((DirentOffset + EtfsFile->DiskOffset), CD_SECTOR_SIZE) ==
|
||||||
EtfsDevice->Offset))
|
EtfsDevice->Offset))
|
||||||
{
|
{
|
||||||
Status = EtfspGetDirent(DirectoryEntry, &Dirent, &DirentOffset);
|
Status = EtfspGetDirent(DirectoryEntry, &Dirent, &DirentOffset);
|
||||||
|
@ -406,6 +432,108 @@ EtfspCachedSearchForDirent (
|
||||||
return Status;
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsRead (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_In_ PVOID Buffer,
|
||||||
|
_In_ ULONG Size,
|
||||||
|
_Out_opt_ PULONG BytesReturned
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ULONG BytesRead;
|
||||||
|
PBL_ETFS_FILE EtfsFile;
|
||||||
|
NTSTATUS Status;
|
||||||
|
|
||||||
|
/* Assume failure for now */
|
||||||
|
BytesRead = 0;
|
||||||
|
|
||||||
|
/* Make sure that the read is within the file's boundaries */
|
||||||
|
EtfsFile = FileEntry->FsSpecificData;
|
||||||
|
if ((Size + EtfsFile->Offset) > EtfsFile->Size)
|
||||||
|
{
|
||||||
|
/* Bail out otherwise */
|
||||||
|
Status = STATUS_INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* Read the offset that matches this file's offset, on the disk */
|
||||||
|
Status = BlDeviceReadAtOffset(FileEntry->DeviceId,
|
||||||
|
Size,
|
||||||
|
EtfsFile->Offset + EtfsFile->DiskOffset,
|
||||||
|
Buffer,
|
||||||
|
&BytesRead);
|
||||||
|
if (NT_SUCCESS(Status))
|
||||||
|
{
|
||||||
|
/* Update the file offset and return the size as having been read */
|
||||||
|
EtfsFile->Offset += Size;
|
||||||
|
BytesRead = Size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check if caller wanted to know how many bytes were read */
|
||||||
|
if (BytesReturned)
|
||||||
|
{
|
||||||
|
/* Return the value */
|
||||||
|
*BytesReturned = BytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All done */
|
||||||
|
return Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsSetInformation (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_In_ PBL_FILE_INFORMATION FileInfo
|
||||||
|
)
|
||||||
|
{
|
||||||
|
PBL_ETFS_FILE EtfsFile;
|
||||||
|
BL_FILE_INFORMATION LocalFileInfo;
|
||||||
|
|
||||||
|
/* Get the underlying ETFS file data structure */
|
||||||
|
EtfsFile = (PBL_ETFS_FILE)FileEntry->FsSpecificData;
|
||||||
|
|
||||||
|
/* Make a copy of the incoming attributes, but ignore the new offset */
|
||||||
|
LocalFileInfo = *FileInfo;
|
||||||
|
LocalFileInfo.Offset = EtfsFile->Offset;
|
||||||
|
|
||||||
|
/* Check if these match exactly the current file */
|
||||||
|
if (!RtlEqualMemory(&LocalFileInfo, &EtfsFile->Size, sizeof(*FileInfo)))
|
||||||
|
{
|
||||||
|
/* Nope -- which means caller is trying to change an immutable */
|
||||||
|
EfiPrintf(L"Incorrect information change\r\n");
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Is the offset past the end of the file? */
|
||||||
|
if (FileInfo->Offset >= EtfsFile->Size)
|
||||||
|
{
|
||||||
|
/* Don't allow EOF */
|
||||||
|
EfiPrintf(L"Offset too large: %lx vs %lx \r\n", FileInfo->Offset, EtfsFile->Size);
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update the offset */
|
||||||
|
EtfsFile->Offset = FileInfo->Offset;
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
NTSTATUS
|
||||||
|
EtfsGetInformation (
|
||||||
|
_In_ PBL_FILE_ENTRY FileEntry,
|
||||||
|
_Out_ PBL_FILE_INFORMATION FileInfo
|
||||||
|
)
|
||||||
|
{
|
||||||
|
PBL_ETFS_FILE EtfsFile;
|
||||||
|
|
||||||
|
/* Get the underlying ETFS file data structure */
|
||||||
|
EtfsFile = (PBL_ETFS_FILE)FileEntry->FsSpecificData;
|
||||||
|
|
||||||
|
/* Copy the cached information structure within it */
|
||||||
|
RtlCopyMemory(FileInfo, &EtfsFile->Size, sizeof(*FileInfo));
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
NTSTATUS
|
NTSTATUS
|
||||||
EtfsOpen (
|
EtfsOpen (
|
||||||
_In_ PBL_FILE_ENTRY Directory,
|
_In_ PBL_FILE_ENTRY Directory,
|
||||||
|
@ -486,7 +614,7 @@ EtfsOpen (
|
||||||
RtlCopyMemory(&NewFile->Callbacks,
|
RtlCopyMemory(&NewFile->Callbacks,
|
||||||
&EtfsFunctionTable,
|
&EtfsFunctionTable,
|
||||||
sizeof(NewFile->Callbacks));
|
sizeof(NewFile->Callbacks));
|
||||||
EtfsFile->Offset = FileOffset;
|
EtfsFile->DiskOffset = FileOffset;
|
||||||
EtfsFile->DirOffset = DirOffset;
|
EtfsFile->DirOffset = DirOffset;
|
||||||
EtfsFile->Size = FileSize;
|
EtfsFile->Size = FileSize;
|
||||||
EtfsFile->DeviceId = DeviceId;
|
EtfsFile->DeviceId = DeviceId;
|
||||||
|
@ -794,7 +922,7 @@ EtfsMount (
|
||||||
RootEntry->FsSpecificData = EtfsFile;
|
RootEntry->FsSpecificData = EtfsFile;
|
||||||
EtfsFile->DeviceId = DeviceId;
|
EtfsFile->DeviceId = DeviceId;
|
||||||
EtfsFile->Flags |= 1;
|
EtfsFile->Flags |= 1;
|
||||||
EtfsFile->Offset = EtfsDevice->RootDirOffset;
|
EtfsFile->DiskOffset = EtfsDevice->RootDirOffset;
|
||||||
EtfsFile->DirOffset = 0;
|
EtfsFile->DirOffset = 0;
|
||||||
EtfsFile->Size = EtfsDevice->RootDirSize;
|
EtfsFile->Size = EtfsDevice->RootDirSize;
|
||||||
EtfsFile->FsName = L"cdfs";
|
EtfsFile->FsName = L"cdfs";
|
||||||
|
|
|
@ -434,7 +434,7 @@ FileOpened:
|
||||||
if (++FileEntry->ReferenceCount == 1)
|
if (++FileEntry->ReferenceCount == 1)
|
||||||
{
|
{
|
||||||
/* Reset unknowns */
|
/* Reset unknowns */
|
||||||
FileEntry->Unknown1 = 0;
|
FileEntry->TotalBytesRead = 0;
|
||||||
FileEntry->Unknown2 = 0;
|
FileEntry->Unknown2 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,7 +548,7 @@ BlFileSetInformation (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Validate file ID */
|
/* Validate file ID */
|
||||||
if (FileEntries > FileId)
|
if (FileId > FileEntries)
|
||||||
{
|
{
|
||||||
return STATUS_INVALID_PARAMETER;
|
return STATUS_INVALID_PARAMETER;
|
||||||
}
|
}
|
||||||
|
@ -579,7 +579,7 @@ BlFileGetInformation (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Validate file ID */
|
/* Validate file ID */
|
||||||
if (FileEntries > FileId)
|
if (FileId > FileEntries)
|
||||||
{
|
{
|
||||||
return STATUS_INVALID_PARAMETER;
|
return STATUS_INVALID_PARAMETER;
|
||||||
}
|
}
|
||||||
|
@ -612,7 +612,7 @@ FileInformationCheck (
|
||||||
Size = 0;
|
Size = 0;
|
||||||
|
|
||||||
/* Make sure we didn't overshoot */
|
/* Make sure we didn't overshoot */
|
||||||
if (FileInformation->CurrentOffset > FileInformation->FileSize)
|
if (FileInformation->Offset > FileInformation->Size)
|
||||||
{
|
{
|
||||||
/* Bail out */
|
/* Bail out */
|
||||||
Status = STATUS_INVALID_PARAMETER;
|
Status = STATUS_INVALID_PARAMETER;
|
||||||
|
@ -621,9 +621,9 @@ FileInformationCheck (
|
||||||
|
|
||||||
/* Compute the appropriate 32-bit size of this read, based on file size */
|
/* Compute the appropriate 32-bit size of this read, based on file size */
|
||||||
Size = ULONG_MAX;
|
Size = ULONG_MAX;
|
||||||
if ((FileInformation->FileSize - FileInformation->CurrentOffset) <= ULONG_MAX)
|
if ((FileInformation->Size - FileInformation->Offset) <= ULONG_MAX)
|
||||||
{
|
{
|
||||||
Size = (ULONG)(FileInformation->FileSize) - (ULONG)(FileInformation->CurrentOffset);
|
Size = (ULONG)(FileInformation->Size) - (ULONG)(FileInformation->Offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check if the caller has an input buffer */
|
/* Check if the caller has an input buffer */
|
||||||
|
@ -683,7 +683,7 @@ BlFileReadEx (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bail out of the file ID is invalid */
|
/* Bail out of the file ID is invalid */
|
||||||
if (FileEntries > FileId)
|
if (FileId > FileEntries)
|
||||||
{
|
{
|
||||||
return STATUS_INVALID_PARAMETER;
|
return STATUS_INVALID_PARAMETER;
|
||||||
}
|
}
|
||||||
|
@ -774,7 +774,7 @@ BlFileReadEx (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Increment the number of bytes read */
|
/* Increment the number of bytes read */
|
||||||
FileEntry->Unknown1 += RequiredSize;
|
FileEntry->TotalBytesRead += RequiredSize;
|
||||||
|
|
||||||
/* Check if the unknown flag on the device was changed during this routine */
|
/* Check if the unknown flag on the device was changed during this routine */
|
||||||
if (ChangedUnknown)
|
if (ChangedUnknown)
|
||||||
|
@ -811,8 +811,8 @@ BlFileReadAtOffsetEx (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Save the current offset, and overwrite it with the one we want */
|
/* Save the current offset, and overwrite it with the one we want */
|
||||||
FileOffset = FileInfo.CurrentOffset;
|
FileOffset = FileInfo.Offset;
|
||||||
FileInfo.CurrentOffset = ByteOffset;
|
FileInfo.Offset = ByteOffset;
|
||||||
|
|
||||||
/* Check the validity of the read and the actual size to read */
|
/* Check the validity of the read and the actual size to read */
|
||||||
RequiredSize = Size;
|
RequiredSize = Size;
|
||||||
|
@ -824,11 +824,12 @@ BlFileReadAtOffsetEx (
|
||||||
if (!NT_SUCCESS(Status))
|
if (!NT_SUCCESS(Status))
|
||||||
{
|
{
|
||||||
/* Bail out if the read is invalid */
|
/* Bail out if the read is invalid */
|
||||||
|
EfiPrintf(L"File info check failure: %lx\n", Status);
|
||||||
return Status;
|
return Status;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Check if the offset we're requesting is not the current offset */
|
/* Check if the offset we're requesting is not the current offset */
|
||||||
if (FileInfo.CurrentOffset != FileOffset)
|
if (FileInfo.Offset != FileOffset)
|
||||||
{
|
{
|
||||||
/* Set the new offset to use */
|
/* Set the new offset to use */
|
||||||
Status = BlFileSetInformation(FileId, &FileInfo);
|
Status = BlFileSetInformation(FileId, &FileInfo);
|
||||||
|
@ -848,10 +849,10 @@ BlFileReadAtOffsetEx (
|
||||||
if (!NT_SUCCESS(Status))
|
if (!NT_SUCCESS(Status))
|
||||||
{
|
{
|
||||||
/* The read failed -- had we modified the offset? */
|
/* The read failed -- had we modified the offset? */
|
||||||
if (FileInfo.CurrentOffset != FileOffset)
|
if (FileInfo.Offset != FileOffset)
|
||||||
{
|
{
|
||||||
/* Restore the offset back to its original value */
|
/* Restore the offset back to its original value */
|
||||||
FileInfo.CurrentOffset = FileOffset;
|
FileInfo.Offset = FileOffset;
|
||||||
BlFileSetInformation(FileId, &FileInfo);
|
BlFileSetInformation(FileId, &FileInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,11 +83,19 @@ BlStatusPrint (
|
||||||
va_start(va, Format);
|
va_start(va, Format);
|
||||||
|
|
||||||
/* Check if the boot debugger is enabled */
|
/* Check if the boot debugger is enabled */
|
||||||
if (BlBdDebuggerEnabled())
|
if (BlBdDebuggerEnabled()
|
||||||
|
#if (defined(DBG))
|
||||||
|
|| TRUE
|
||||||
|
#endif
|
||||||
|
)
|
||||||
{
|
{
|
||||||
/* Print the string out into a buffer */
|
/* Print the string out into a buffer */
|
||||||
if (vswprintf(BlScratchBuffer, Format, va) > 0)
|
if (vswprintf(BlScratchBuffer, Format, va) > 0)
|
||||||
{
|
{
|
||||||
|
#if defined(DBG)
|
||||||
|
EfiPrintf(BlScratchBuffer);
|
||||||
|
EfiPrintf(L"\r\n");
|
||||||
|
#endif
|
||||||
/* Make it a UNICODE_STRING */
|
/* Make it a UNICODE_STRING */
|
||||||
RtlInitUnicodeString(&UnicodeString, BlScratchBuffer);
|
RtlInitUnicodeString(&UnicodeString, BlScratchBuffer);
|
||||||
|
|
||||||
|
|
|
@ -39,8 +39,8 @@ ImgpGetFileSize (
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We only support files less than 4GB in the Image Mapped */
|
/* We only support files less than 4GB in the Image Mapped */
|
||||||
Size = FileInformation.FileSize;
|
Size = FileInformation.Size;
|
||||||
if (FileInformation.FileSize > ULONG_MAX)
|
if (FileInformation.Size > ULONG_MAX)
|
||||||
{
|
{
|
||||||
return STATUS_NOT_SUPPORTED;
|
return STATUS_NOT_SUPPORTED;
|
||||||
}
|
}
|
||||||
|
@ -392,6 +392,7 @@ BlImgLoadImageWithProgress2 (
|
||||||
Status = ImgpOpenFile(DeviceId, FileName, DeviceId, &FileHandle);
|
Status = ImgpOpenFile(DeviceId, FileName, DeviceId, &FileHandle);
|
||||||
if (!NT_SUCCESS(Status))
|
if (!NT_SUCCESS(Status))
|
||||||
{
|
{
|
||||||
|
EfiPrintf(L"Error opening file: %lx\r\n", Status);
|
||||||
goto Quickie;
|
goto Quickie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,6 +400,7 @@ BlImgLoadImageWithProgress2 (
|
||||||
Status = ImgpGetFileSize(&FileHandle, &ImageSize);
|
Status = ImgpGetFileSize(&FileHandle, &ImageSize);
|
||||||
if (!NT_SUCCESS(Status))
|
if (!NT_SUCCESS(Status))
|
||||||
{
|
{
|
||||||
|
EfiPrintf(L"Error getting file size: %lx\r\n", Status);
|
||||||
goto Quickie;
|
goto Quickie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue