Added hybrid MBR and destroy-GPT options
This commit is contained in:
18
CHANGELOG
18
CHANGELOG
@@ -3,6 +3,19 @@
|
||||
|
||||
- Changed __DARWIN_UNIX03 to __APPLE__ as code to enable MacOS X support.
|
||||
|
||||
- Added the ability to create a hybrid MBR ('h' on experts' menu). This was
|
||||
motivated by my discovery that Windows 7 remains brain-dead when it comes
|
||||
to the ability to boot from a GPT disk, at least on BIOS-based machines.
|
||||
|
||||
- Added 'z' option to experts' menu, to destroy GPT data structures and
|
||||
exit. The intent is to use this feature to enable subsequent partitioning
|
||||
of the disk using fdisk or other GPT-unaware tools. (GNU Parted will wipe
|
||||
the GPT data structures itself when you create a new MBR ["msdos
|
||||
disklabel," in Parted parlance], so using Parted is another option.)
|
||||
|
||||
- Slightly altered the effect of the 'o' command on the main menu. It now
|
||||
blanks out the protective MBR, as well as the GPT data.
|
||||
|
||||
0.3.1:
|
||||
------
|
||||
|
||||
@@ -15,11 +28,6 @@
|
||||
work OK on my Mac OS test system and on both 32- and 64-bit Linux
|
||||
systems.
|
||||
|
||||
- Added test for writability when opening a disk for reading. If the
|
||||
test fails, a warning message is displayed. (The test simply opens
|
||||
the disk for writing and then closes it before writing any data,
|
||||
so the test shouldn't cause problems.)
|
||||
|
||||
- Fixed off-by-one bug in GPTData::FindLastAvailable().
|
||||
|
||||
- Fixed bug that caused display of options after a disk-write error.
|
||||
|
||||
35
gdisk.8
35
gdisk.8
@@ -405,12 +405,14 @@ there will be no backup partition table on disk.
|
||||
Use main GPT header and rebuild the backup. This option is likely to be
|
||||
useful if the backup GPT header has been damaged or destroyed.
|
||||
.TP
|
||||
|
||||
.B e
|
||||
Load main partition table. This option reloads the main partition table
|
||||
from disk. It's only likely to be useful if you've tried to use the backup
|
||||
partition table (via 'c') but it's in worse shape then the main partition
|
||||
table.
|
||||
.TP
|
||||
|
||||
.B f
|
||||
Change partition GUID. You can enter a custom unique GUID for a partition
|
||||
using this option. (Note this refers to the GUID that uniquely identifies a
|
||||
@@ -424,6 +426,16 @@ Change disk GUID. Each disk has a unique GUID code, which
|
||||
.B gdisk
|
||||
assigns randomly upon creation of the GPT data structures. You can generate
|
||||
a fresh random GUID or enter one manually with this option.
|
||||
|
||||
.TP
|
||||
.B h
|
||||
Create a hybrid MBR. This is an ugly workaround that enables GPT-unaware
|
||||
OSes, or that that can't boot from a GPT disk, to access up to three of
|
||||
the partitions on the disk by creating MBR entries for them. Note that
|
||||
these hybrid MBR entries are not updated when you make subsequent changes
|
||||
to the GPT entries, so you must re-run this option whenever you make
|
||||
changes that would affect the hybridized partitions.
|
||||
|
||||
.TP
|
||||
.B i
|
||||
Show detailed partition information. This option is identical to the 'i'
|
||||
@@ -480,6 +492,16 @@ Verify disk. This option is identical to the 'v' option in the main menu.
|
||||
.B w
|
||||
Write table to disk and exit. This option is identical to the 'w' option in
|
||||
the main menu.
|
||||
|
||||
.TP
|
||||
.B z
|
||||
Destroy the GPT data structures and exit. Use this option if you want to
|
||||
repartition a GPT disk using
|
||||
.B "fdisk"
|
||||
or some other GPT-unaware program.
|
||||
You'll be given the choice of preserving the existing MBR, in case it's a
|
||||
hybrid MBR with salvageable partitions.
|
||||
|
||||
.PP
|
||||
|
||||
In many cases, you can press the Enter key to select a default option when
|
||||
@@ -488,7 +510,7 @@ entering data. When only one option is possible,
|
||||
usually bypasses the prompt entirely.
|
||||
|
||||
.SH BUGS
|
||||
As of August of 2009 (version 0.3.1),
|
||||
As of August of 2009 (version 0.3.2),
|
||||
.B gdisk
|
||||
should be considered early beta software. Known bugs and
|
||||
limitations include:
|
||||
@@ -506,12 +528,6 @@ The program compiles correctly only on Linux and Mac OS X. Both 64-bit
|
||||
more thoroughly than the latter. The Mac OS X support was added with
|
||||
version 0.3.1 and has not been thoroughly tested.
|
||||
|
||||
.TP
|
||||
.B *
|
||||
Under Mac OS X, the program will only save a partition table if no
|
||||
partitions from the disk are currently mounted. (This limitation does not
|
||||
exist in the Linux version of the program.)
|
||||
|
||||
.TP
|
||||
.B *
|
||||
The fields used to display the start and end sector numbers for partitions
|
||||
@@ -571,7 +587,10 @@ get appropriate GUID type codes at all.
|
||||
Booting after converting an MBR disk may be disrupted. Sometimes
|
||||
re-installing a boot loader will fix the problem, but other times you may
|
||||
need to switch boot loaders. Except on EFI-based platforms, Windows through
|
||||
Vista doesn't support booting from GPT disks.
|
||||
at least Windows 7 RC doesn't support booting from GPT disks. Creating a
|
||||
hybrid MBR (using the 'h' option on the experts' menu) or abandoning GPT in
|
||||
favor of MBR may be your only options in this case.
|
||||
|
||||
.PP
|
||||
|
||||
.SH AUTHORS
|
||||
|
||||
17
gdisk.cc
17
gdisk.cc
@@ -24,7 +24,7 @@ int main(int argc, char* argv[]) {
|
||||
int doMore = 1;
|
||||
char* device = NULL;
|
||||
|
||||
printf("GPT fdisk (gdisk) version 0.3.1\n\n");
|
||||
printf("GPT fdisk (gdisk) version 0.3.2\n\n");
|
||||
|
||||
if (argc == 2) { // basic usage
|
||||
if (SizesOK()) {
|
||||
@@ -88,7 +88,7 @@ int DoCommand(char* filename, struct GPTData* theGPT) {
|
||||
break;
|
||||
case 'o': case 'O':
|
||||
theGPT->ClearGPTData();
|
||||
// theGPT->protectiveMBR.MakeProtectiveMBR();
|
||||
theGPT->MakeProtectiveMBR();
|
||||
// theGPT->BlankPartitions();
|
||||
break;
|
||||
case 'p': case 'P':
|
||||
@@ -192,9 +192,9 @@ int ExpertsMenu(char* filename, struct GPTData* theGPT) {
|
||||
printf("Enter the disk's unique GUID:\n");
|
||||
theGPT->SetDiskGUID(GetGUID());
|
||||
break;
|
||||
/* case 'h': case 'H':
|
||||
case 'h': case 'H':
|
||||
theGPT->MakeHybrid();
|
||||
break; */
|
||||
break;
|
||||
case 'i': case 'I':
|
||||
theGPT->ShowDetails();
|
||||
break;
|
||||
@@ -238,6 +238,12 @@ int ExpertsMenu(char* filename, struct GPTData* theGPT) {
|
||||
goOn = 0;
|
||||
} // if
|
||||
break;
|
||||
case 'z': case 'Z':
|
||||
if (theGPT->DestroyGPT() == 1) {
|
||||
retval = 0;
|
||||
goOn = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ShowExpertCommands();
|
||||
break;
|
||||
@@ -254,7 +260,7 @@ void ShowExpertCommands(void) {
|
||||
printf("e\tload main partition table from disk (rebuilding backup)\n");
|
||||
printf("f\tchange partition GUID\n");
|
||||
printf("g\tchange disk GUID\n");
|
||||
// printf("h\tmake hybrid MBR\n");
|
||||
printf("h\tmake hybrid MBR\n");
|
||||
printf("i\tshow detailed information on a partition\n");
|
||||
printf("k\tsave partition data to a backup file\n");
|
||||
printf("l\tload partition data from a backup file\n");
|
||||
@@ -267,4 +273,5 @@ void ShowExpertCommands(void) {
|
||||
printf("s\tresize partition table\n");
|
||||
printf("v\tverify disk\n");
|
||||
printf("w\twrite table to disk and exit\n");
|
||||
printf("z\tDestroy GPT data structures and exit\n");
|
||||
} // ShowExpertCommands()
|
||||
|
||||
107
gpt.cc
107
gpt.cc
@@ -1307,7 +1307,6 @@ uint64_t GPTData::FindFreeBlocks(int *numSegments, uint64_t *largestSegment) {
|
||||
return totalFound;
|
||||
} // GPTData::FindFreeBlocks()
|
||||
|
||||
/*
|
||||
// Create a hybrid MBR -- an ugly, funky thing that helps GPT work with
|
||||
// OSes that don't understand GPT.
|
||||
void GPTData::MakeHybrid(void) {
|
||||
@@ -1315,16 +1314,26 @@ void GPTData::MakeHybrid(void) {
|
||||
char line[255];
|
||||
int numParts, i, j, typeCode, bootable;
|
||||
uint64_t length;
|
||||
char fillItUp = 'M'; // fill extra partition entries? (Yes/No/Maybe)
|
||||
|
||||
// First, rebuild the protective MBR...
|
||||
protectiveMBR.MakeProtectiveMBR();
|
||||
printf("\nWARNING! Hybrid MBRs are flaky and potentially dangerous! If you decide not\n"
|
||||
"to use one, just hit the Enter key at the below prompt and your MBR\n"
|
||||
"partition table will be untouched.\n\n\a");
|
||||
|
||||
// Now get the numbers of up to three partitions to add to the
|
||||
// hybrid MBR....
|
||||
printf("Type from one to three partition numbers to be added to the hybrid MBR, in\n"
|
||||
"sequence: ");
|
||||
printf("Type from one to three GPT partition numbers, separated by spaces, to be\n"
|
||||
"added to the hybrid MBR, in sequence: ");
|
||||
fgets(line, 255, stdin);
|
||||
numParts = sscanf(line, "%d %d %d", &partNums[0], &partNums[1], &partNums[2]);
|
||||
|
||||
if (numParts > 0) {
|
||||
// Blank out the protective MBR, but leave the boot loader code
|
||||
// alone....
|
||||
protectiveMBR.EmptyMBR(0);
|
||||
protectiveMBR.SetDiskSize(diskSize);
|
||||
} // if
|
||||
|
||||
for (i = 0; i < numParts; i++) {
|
||||
j = partNums[i] - 1;
|
||||
printf("Creating entry for partition #%d\n", j + 1);
|
||||
@@ -1337,7 +1346,7 @@ void GPTData::MakeHybrid(void) {
|
||||
printf("Set the bootable flag? ");
|
||||
bootable = (GetYN() == 'Y');
|
||||
length = partitions[j].lastLBA - partitions[j].firstLBA + UINT64_C(1);
|
||||
protectiveMBR.MakePart(i + 1, (uint32_t) partitions[j].firstLBA,
|
||||
protectiveMBR.MakePart(i, (uint32_t) partitions[j].firstLBA,
|
||||
(uint32_t) length, typeCode, bootable);
|
||||
} else { // partition out of range
|
||||
printf("Partition %d ends beyond the 2TiB limit of MBR partitions; omitting it.\n",
|
||||
@@ -1347,8 +1356,46 @@ void GPTData::MakeHybrid(void) {
|
||||
printf("Partition %d is out of range; omitting it.\n", j + 1);
|
||||
} // if/else
|
||||
} // for
|
||||
|
||||
if (numParts > 0) { // User opted to create a hybrid MBR....
|
||||
// Create EFI protective partition that covers the start of the disk.
|
||||
// If this location (covering the main GPT data structures) is omitted,
|
||||
// Linux won't find any partitions on the disk. Note that this is
|
||||
// NUMBERED AFTER the hybrid partitions, contrary to what the
|
||||
// gptsync utility does. This is because Windows seems to choke on
|
||||
// disks with a 0xEE partition in the first slot and subsequent
|
||||
// additional partitions, unless it boots from the disk.
|
||||
protectiveMBR.MakePart(numParts, 1, protectiveMBR.FindLastInFree(1), 0xEE);
|
||||
|
||||
// ... and for good measure, if there are any partition spaces left,
|
||||
// optionally create more protective EFI partitions to cover as much
|
||||
// space as possible....
|
||||
for (i = 0; i < 4; i++) {
|
||||
if (protectiveMBR.GetType(i) == 0x00) { // unused entry....
|
||||
if (fillItUp == 'M') {
|
||||
printf("Unused partition space(s) found. Use one to protect more partitions? ");
|
||||
fillItUp = GetYN();
|
||||
typeCode = 0x00; // use this to flag a need to get type code
|
||||
} // if
|
||||
if (fillItUp == 'Y') {
|
||||
if (typeCode == 0x00) {
|
||||
printf("Enter an MBR hex code (EE is EFI GPT, but may confuse MacOS): ");
|
||||
// Comment on above: Mac OS treats disks with more than one
|
||||
// 0xEE MBR partition as MBR disks, not as GPT disks.
|
||||
fgets(line, 255, stdin);
|
||||
sscanf(line, "%x", &typeCode);
|
||||
} // if (typeCode == 0x00)
|
||||
protectiveMBR.MakeBiggestPart(i, typeCode); // make a partition
|
||||
} // if (fillItUp == 'Y')
|
||||
} // if unused entry
|
||||
} // for (i = 0; i < 4; i++)
|
||||
} // if (numParts > 0)
|
||||
} // GPTData::MakeHybrid()
|
||||
*/
|
||||
|
||||
// Create a fresh protective MBR.
|
||||
void GPTData::MakeProtectiveMBR(void) {
|
||||
protectiveMBR.MakeProtectiveMBR();
|
||||
} // GPTData::MakeProtectiveMBR(void)
|
||||
|
||||
// Writes GPT (and protective MBR) to disk. Returns 1 on successful
|
||||
// write, 0 if there was a problem.
|
||||
@@ -1609,6 +1656,52 @@ int GPTData::LoadGPTBackup(char* filename) {
|
||||
return allOK;
|
||||
} // GPTData::LoadGPTBackup()
|
||||
|
||||
// This function destroys the on-disk GPT structures. Returns 1 if the
|
||||
// user confirms destruction, 0 if the user aborts.
|
||||
int GPTData::DestroyGPT(void) {
|
||||
int fd, i, doMore;
|
||||
char blankSector[512], goOn;
|
||||
|
||||
for (i = 0; i < 512; i++) {
|
||||
blankSector[i] = '\0';
|
||||
} // for
|
||||
|
||||
printf("\a\aAbout to wipe out GPT on %s. Proceed? ", device);
|
||||
goOn = GetYN();
|
||||
if (goOn == 'Y') {
|
||||
fd = open(device, O_WRONLY);
|
||||
#ifdef __APPLE__
|
||||
// MacOS X requires a shared lock under some circumstances....
|
||||
if (fd < 0) {
|
||||
fd = open(device, O_WRONLY|O_SHLOCK);
|
||||
} // if
|
||||
#endif
|
||||
if (fd != -1) {
|
||||
lseek64(fd, mainHeader.currentLBA * 512, SEEK_SET); // seek to GPT header
|
||||
write(fd, blankSector, 512); // blank it out
|
||||
lseek64(fd, mainHeader.partitionEntriesLBA * 512, SEEK_SET); // seek to partition table
|
||||
for (i = 0; i < GetBlocksInPartTable(); i++)
|
||||
write(fd, blankSector, 512);
|
||||
lseek64(fd, secondHeader.partitionEntriesLBA * 512, SEEK_SET); // seek to partition table
|
||||
for (i = 0; i < GetBlocksInPartTable(); i++)
|
||||
write(fd, blankSector, 512);
|
||||
lseek64(fd, secondHeader.currentLBA * 512, SEEK_SET); // seek to GPT header
|
||||
write(fd, blankSector, 512); // blank it out
|
||||
printf("Blank out MBR? ");
|
||||
if (GetYN() == 'Y') {
|
||||
lseek64(fd, 0, SEEK_SET);
|
||||
write(fd, blankSector, 512); // blank it out
|
||||
} // if blank MBR
|
||||
close(fd);
|
||||
printf("GPT data structures destroyed! You may now partition the disk using fdisk or\n"
|
||||
"other utilities. Program will now terminate.\n");
|
||||
} else {
|
||||
printf("Problem opening %s for writing! Program will now terminate.\n");
|
||||
} // if/else (fd != -1)
|
||||
} // if (goOn == 'Y')
|
||||
return (goOn == 'Y');
|
||||
} // GPTData::DestroyGPT()
|
||||
|
||||
// Check to be sure that data type sizes are correct. The basic types (uint*_t) should
|
||||
// never fail these tests, but the struct types may fail depending on compile options.
|
||||
// Specifically, the -fpack-struct option to gcc may be required to ensure proper structure
|
||||
|
||||
5
gpt.h
5
gpt.h
@@ -126,11 +126,12 @@ public:
|
||||
void RebuildSecondHeader(void);
|
||||
void LoadSecondTableAsMain(void);
|
||||
uint64_t FindFreeBlocks(int *numSegments, uint64_t *largestSegment);
|
||||
// void MakeHybrid(void);
|
||||
void MakeProtectiveMBR(void) {return protectiveMBR.MakeProtectiveMBR();}
|
||||
void MakeHybrid(void);
|
||||
void MakeProtectiveMBR(void);
|
||||
int SaveGPTData(void);
|
||||
int SaveGPTBackup(char* filename);
|
||||
int LoadGPTBackup(char* filename);
|
||||
int DestroyGPT(void); // Returns 1 if user proceeds
|
||||
|
||||
// Return data about the GPT structures....
|
||||
uint32_t GetNumParts(void) {return mainHeader.numParts;}
|
||||
|
||||
92
mbr.cc
92
mbr.cc
@@ -52,14 +52,22 @@ MBRData::MBRData(char *filename) {
|
||||
MBRData::~MBRData(void) {
|
||||
} // MBRData destructor
|
||||
|
||||
// Empty all data. Meant mainly for calling by constructors
|
||||
void MBRData::EmptyMBR(void) {
|
||||
// Empty all data. Meant mainly for calling by constructors, but it's also
|
||||
// used by the hybrid MBR functions in the GPTData class.
|
||||
void MBRData::EmptyMBR(int clearBootloader) {
|
||||
int i;
|
||||
|
||||
// Zero out the boot loader section, the disk signature, and the
|
||||
// 2-byte nulls area only if requested to do so. (This is the
|
||||
// default.)
|
||||
if (clearBootloader == 1) {
|
||||
for (i = 0; i < 440; i++)
|
||||
code[i] = 0;
|
||||
diskSignature = (uint32_t) rand();
|
||||
nulls = 0;
|
||||
} // if
|
||||
|
||||
// Blank out the partitions
|
||||
for (i = 0; i < 4; i++) {
|
||||
partitions[i].status = UINT8_C(0);
|
||||
partitions[i].firstSector[0] = UINT8_C(0);
|
||||
@@ -343,6 +351,39 @@ void MBRData::MakeProtectiveMBR(void) {
|
||||
state = gpt;
|
||||
} // MBRData::MakeProtectiveMBR()
|
||||
|
||||
// Create a partition that fills the most available space. Returns
|
||||
// 1 if partition was created, 0 otherwise. Intended for use in
|
||||
// creating hybrid MBRs.
|
||||
int MBRData::MakeBiggestPart(int i, int type) {
|
||||
uint32_t start = UINT32_C(1); // starting point for each search
|
||||
uint32_t firstBlock; // first block in a segment
|
||||
uint32_t lastBlock; // last block in a segment
|
||||
uint32_t segmentSize; // size of segment in blocks
|
||||
uint32_t selectedSegment = UINT32_C(0); // location of largest segment
|
||||
uint32_t selectedSize = UINT32_C(0); // size of largest segment in blocks
|
||||
int found = 0;
|
||||
|
||||
do {
|
||||
firstBlock = FindFirstAvailable(start);
|
||||
if (firstBlock != UINT32_C(0)) { // something's free...
|
||||
lastBlock = FindLastInFree(firstBlock);
|
||||
segmentSize = lastBlock - firstBlock + UINT32_C(1);
|
||||
if (segmentSize > selectedSize) {
|
||||
selectedSize = segmentSize;
|
||||
selectedSegment = firstBlock;
|
||||
} // if
|
||||
start = lastBlock + 1;
|
||||
} // if
|
||||
} while (firstBlock != 0);
|
||||
if ((selectedSize > UINT32_C(0)) && ((uint64_t) selectedSize < diskSize)) {
|
||||
found = 1;
|
||||
MakePart(i, selectedSegment, selectedSize, type, 0);
|
||||
} else {
|
||||
found = 0;
|
||||
} // if/else
|
||||
return found;
|
||||
} // MBRData::MakeBiggestPart(int i)
|
||||
|
||||
// Return a pointer to a primary or logical partition, or NULL if
|
||||
// the partition is out of range....
|
||||
struct MBRRecord* MBRData::GetPartition(int i) {
|
||||
@@ -398,6 +439,53 @@ void MBRData::MakePart(int num, uint32_t start, uint32_t length, int type,
|
||||
partitions[num].lengthLBA = length;
|
||||
} // MakePart()
|
||||
|
||||
// Finds the first free space on the disk from start onward; returns 0
|
||||
// if none available....
|
||||
uint32_t MBRData::FindFirstAvailable(uint32_t start) {
|
||||
uint32_t first;
|
||||
uint32_t i;
|
||||
int firstMoved = 0;
|
||||
|
||||
first = start;
|
||||
|
||||
// ...now search through all partitions; if first is within an
|
||||
// existing partition, move it to the next sector after that
|
||||
// partition and repeat. If first was moved, set firstMoved
|
||||
// flag; repeat until firstMoved is not set, so as to catch
|
||||
// cases where partitions are out of sequential order....
|
||||
do {
|
||||
firstMoved = 0;
|
||||
for (i = 0; i < 4; i++) {
|
||||
// Check if it's in the existing partition
|
||||
if ((first >= partitions[i].firstLBA) &&
|
||||
(first < (partitions[i].firstLBA + partitions[i].lengthLBA))) {
|
||||
first = partitions[i].firstLBA + partitions[i].lengthLBA;
|
||||
firstMoved = 1;
|
||||
} // if
|
||||
} // for
|
||||
} while (firstMoved == 1);
|
||||
if (first >= diskSize)
|
||||
first = 0;
|
||||
return (first);
|
||||
} // MBRData::FindFirstAvailable()
|
||||
|
||||
uint32_t MBRData::FindLastInFree(uint32_t start) {
|
||||
uint32_t nearestStart;
|
||||
uint32_t i;
|
||||
|
||||
if (diskSize <= UINT32_MAX)
|
||||
nearestStart = diskSize - 1;
|
||||
else
|
||||
nearestStart = UINT32_MAX - 1;
|
||||
for (i = 0; i < 4; i++) {
|
||||
if ((nearestStart > partitions[i].firstLBA) &&
|
||||
(partitions[i].firstLBA > start)) {
|
||||
nearestStart = partitions[i].firstLBA - 1;
|
||||
} // if
|
||||
} // for
|
||||
return (nearestStart);
|
||||
} // MBRData::FindLastInFree
|
||||
|
||||
uint8_t MBRData::GetStatus(int i) {
|
||||
MBRRecord* thePart;
|
||||
uint8_t retval;
|
||||
|
||||
9
mbr.h
9
mbr.h
@@ -71,7 +71,9 @@ public:
|
||||
MBRData(void);
|
||||
MBRData(char* deviceFilename);
|
||||
~MBRData(void);
|
||||
void EmptyMBR(void);
|
||||
// Pass EmptyMBR 1 to clear the boot loader code, 0 to leave it intact
|
||||
void EmptyMBR(int clearBootloader = 1);
|
||||
void SetDiskSize(uint64_t ds) {diskSize = ds;}
|
||||
int ReadMBRData(char* deviceFilename);
|
||||
void ReadMBRData(int fd);
|
||||
int WriteMBRData(void);
|
||||
@@ -84,6 +86,11 @@ public:
|
||||
void ShowState(void);
|
||||
void MakePart(int num, uint32_t startLBA, uint32_t lengthLBA, int type = 0x07,
|
||||
int bootable = 0);
|
||||
int MakeBiggestPart(int i, int type); // Make partition filling most space
|
||||
|
||||
// Functions to find information on free space....
|
||||
uint32_t FindFirstAvailable(uint32_t start = 1);
|
||||
uint32_t FindLastInFree(uint32_t start);
|
||||
|
||||
// Functions to extract data on specific partitions....
|
||||
uint8_t GetStatus(int i);
|
||||
|
||||
Reference in New Issue
Block a user