mirror of
https://github.com/dmolony/DiskBrowser.git
synced 2024-11-16 04:04:39 +00:00
Split NuFX
This commit is contained in:
parent
ce16479742
commit
2168025e80
@ -152,7 +152,7 @@ public class DiskFactory
|
||||
System.out.println (" ** sdk **");
|
||||
try
|
||||
{
|
||||
NuFX nuFX = new NuFX (file);
|
||||
NuFX nuFX = new NuFX (file.toPath ());
|
||||
File tmp = File.createTempFile ("sdk", null);
|
||||
FileOutputStream fos = new FileOutputStream (tmp);
|
||||
fos.write (nuFX.getBuffer ());
|
||||
|
96
src/com/bytezone/diskbrowser/utilities/Header.java
Normal file
96
src/com/bytezone/diskbrowser/utilities/Header.java
Normal file
@ -0,0 +1,96 @@
|
||||
package com.bytezone.diskbrowser.utilities;
|
||||
|
||||
// -----------------------------------------------------------------------------------//
|
||||
class Header
|
||||
// -----------------------------------------------------------------------------------//
|
||||
{
|
||||
private final int totalRecords;
|
||||
private final int version;
|
||||
private final int eof;
|
||||
private final int crc;
|
||||
private final DateTime created;
|
||||
private final DateTime modified;
|
||||
boolean bin2;
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
public Header (byte[] buffer) throws FileFormatException
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
int ptr = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (isNuFile (buffer, ptr))
|
||||
break;
|
||||
|
||||
if (isBin2 (buffer, ptr))
|
||||
{
|
||||
ptr += 128;
|
||||
bin2 = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new FileFormatException ("NuFile not found");
|
||||
}
|
||||
|
||||
crc = Utility.getWord (buffer, ptr + 6);
|
||||
totalRecords = Utility.getLong (buffer, ptr + 8);
|
||||
created = new DateTime (buffer, ptr + 12);
|
||||
modified = new DateTime (buffer, ptr + 20);
|
||||
version = Utility.getWord (buffer, ptr + 28);
|
||||
eof = Utility.getLong (buffer, ptr + 38);
|
||||
|
||||
byte[] crcBuffer = new byte[40];
|
||||
System.arraycopy (buffer, ptr + 8, crcBuffer, 0, crcBuffer.length);
|
||||
if (crc != Utility.getCRC (crcBuffer, 0))
|
||||
{
|
||||
System.out.println ("***** Master CRC mismatch *****");
|
||||
throw new FileFormatException ("Master CRC failed");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
int getTotalRecords ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
return totalRecords;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
private boolean isNuFile (byte[] buffer, int ptr)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46
|
||||
&& buffer[ptr + 3] == (byte) 0xE9 && buffer[ptr + 4] == 0x6C
|
||||
&& buffer[ptr + 5] == (byte) 0xE5)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
private boolean isBin2 (byte[] buffer, int ptr)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
if (buffer[ptr] == 0x0A && buffer[ptr + 1] == 0x47 && buffer[ptr + 2] == 0x4C
|
||||
&& buffer[ptr + 18] == (byte) 0x02)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
@Override
|
||||
public String toString ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
StringBuilder text = new StringBuilder ();
|
||||
|
||||
text.append (String.format ("Master CRC ..... %,d (%04X)%n", crc, crc));
|
||||
text.append (String.format ("Records ........ %,d%n", totalRecords));
|
||||
text.append (String.format ("Created ........ %s%n", created.format ()));
|
||||
text.append (String.format ("Modified ....... %s%n", modified.format ()));
|
||||
text.append (String.format ("Version ........ %,d%n", version));
|
||||
text.append (String.format ("Master EOF ..... %,d", eof));
|
||||
|
||||
return text.toString ();
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ class LZW
|
||||
for (byte[] track : chunks)
|
||||
System.arraycopy (track, 0, buffer, trackNumber++ * TRACK_LENGTH, TRACK_LENGTH);
|
||||
|
||||
if (crc != NuFX.getCRC (buffer, crcBase))
|
||||
if (crc != Utility.getCRC (buffer, crcBase))
|
||||
System.out.println ("\n*** LZW CRC mismatch ***");
|
||||
|
||||
return buffer;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.bytezone.diskbrowser.utilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@ -11,10 +10,6 @@ import java.util.List;
|
||||
public class NuFX
|
||||
// -----------------------------------------------------------------------------------//
|
||||
{
|
||||
private static String[] fileSystems =
|
||||
{ "", "ProDOS/SOS", "DOS 3.3", "DOS 3.2", "Apple II Pascal", "Macintosh HFS",
|
||||
"Macintosh MFS", "Lisa File System", "Apple CP/M", "", "MS-DOS", "High Sierra",
|
||||
"ISO 9660", "AppleShare" };
|
||||
private Header header;
|
||||
private final byte[] buffer;
|
||||
private final boolean debug = false;
|
||||
@ -30,14 +25,6 @@ public class NuFX
|
||||
readBuffer ();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
public NuFX (File file) throws FileFormatException, IOException
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
buffer = Files.readAllBytes (file.toPath ());
|
||||
readBuffer ();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
private void readBuffer ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
@ -51,19 +38,19 @@ public class NuFX
|
||||
if (debug)
|
||||
System.out.printf ("%s%n%n", header);
|
||||
|
||||
for (int rec = 0; rec < header.totalRecords; rec++)
|
||||
for (int rec = 0; rec < header.getTotalRecords (); rec++)
|
||||
{
|
||||
Record record = new Record (dataPtr);
|
||||
Record record = new Record (buffer, dataPtr);
|
||||
records.add (record);
|
||||
|
||||
if (debug)
|
||||
System.out.printf ("Record: %d%n%n%s%n%n", rec, record);
|
||||
|
||||
dataPtr += record.attributes + record.fileNameLength;
|
||||
dataPtr += record.getAttributes () + record.getFileNameLength ();
|
||||
int threadsPtr = dataPtr;
|
||||
dataPtr += record.totThreads * 16;
|
||||
dataPtr += record.getTotalThreads () * 16;
|
||||
|
||||
for (int i = 0; i < record.totThreads; i++)
|
||||
for (int i = 0; i < record.getTotalThreads (); i++)
|
||||
{
|
||||
Thread thread = new Thread (buffer, threadsPtr + i * 16, dataPtr);
|
||||
threads.add (thread);
|
||||
@ -95,202 +82,4 @@ public class NuFX
|
||||
return thread.toString ();
|
||||
return "no disk";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
protected static int getCRC (final byte[] buffer, int base)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
int crc = base;
|
||||
for (int j = 0; j < buffer.length; j++)
|
||||
{
|
||||
crc = ((crc >>> 8) | (crc << 8)) & 0xFFFF;
|
||||
crc ^= (buffer[j] & 0xFF);
|
||||
crc ^= ((crc & 0xFF) >>> 4);
|
||||
crc ^= (crc << 12) & 0xFFFF;
|
||||
crc ^= ((crc & 0xFF) << 5) & 0xFFFF;
|
||||
}
|
||||
|
||||
crc &= 0xFFFF;
|
||||
return crc;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
class Header
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
private final int totalRecords;
|
||||
private final int version;
|
||||
private final int eof;
|
||||
private final int crc;
|
||||
private final DateTime created;
|
||||
private final DateTime modified;
|
||||
boolean bin2;
|
||||
|
||||
public Header (byte[] buffer) throws FileFormatException
|
||||
{
|
||||
int ptr = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (isNuFile (buffer, ptr))
|
||||
break;
|
||||
|
||||
if (isBin2 (buffer, ptr))
|
||||
{
|
||||
ptr += 128;
|
||||
bin2 = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new FileFormatException ("NuFile not found");
|
||||
}
|
||||
|
||||
crc = Utility.getWord (buffer, ptr + 6);
|
||||
totalRecords = Utility.getLong (buffer, ptr + 8);
|
||||
created = new DateTime (buffer, ptr + 12);
|
||||
modified = new DateTime (buffer, ptr + 20);
|
||||
version = Utility.getWord (buffer, ptr + 28);
|
||||
eof = Utility.getLong (buffer, ptr + 38);
|
||||
|
||||
byte[] crcBuffer = new byte[40];
|
||||
System.arraycopy (buffer, ptr + 8, crcBuffer, 0, crcBuffer.length);
|
||||
if (crc != getCRC (crcBuffer, 0))
|
||||
{
|
||||
System.out.println ("***** Master CRC mismatch *****");
|
||||
throw new FileFormatException ("Master CRC failed");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNuFile (byte[] buffer, int ptr)
|
||||
{
|
||||
if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46
|
||||
&& buffer[ptr + 3] == (byte) 0xE9 && buffer[ptr + 4] == 0x6C
|
||||
&& buffer[ptr + 5] == (byte) 0xE5)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isBin2 (byte[] buffer, int ptr)
|
||||
{
|
||||
if (buffer[ptr] == 0x0A && buffer[ptr + 1] == 0x47 && buffer[ptr + 2] == 0x4C
|
||||
&& buffer[ptr + 18] == (byte) 0x02)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
StringBuilder text = new StringBuilder ();
|
||||
|
||||
text.append (String.format ("Master CRC ..... %,d (%04X)%n", crc, crc));
|
||||
text.append (String.format ("Records ........ %,d%n", totalRecords));
|
||||
text.append (String.format ("Created ........ %s%n", created.format ()));
|
||||
text.append (String.format ("Modified ....... %s%n", modified.format ()));
|
||||
text.append (String.format ("Version ........ %,d%n", version));
|
||||
text.append (String.format ("Master EOF ..... %,d", eof));
|
||||
|
||||
return text.toString ();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
class Record
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
private final int totThreads;
|
||||
private final int crc;
|
||||
private final char separator;
|
||||
private final int fileSystemID;
|
||||
private final int attributes;
|
||||
private final int version;
|
||||
private final int access;
|
||||
private final int fileType;
|
||||
private final int auxType;
|
||||
private final int storType;
|
||||
private final DateTime created;
|
||||
private final DateTime modified;
|
||||
private final DateTime archived;
|
||||
private final int optionSize;
|
||||
private final int fileNameLength;
|
||||
private final String fileName;
|
||||
|
||||
public Record (int dataPtr) throws FileFormatException
|
||||
{
|
||||
// check for NuFX
|
||||
if (!isNuFX (buffer, dataPtr))
|
||||
throw new FileFormatException ("NuFX not found");
|
||||
|
||||
crc = Utility.getWord (buffer, dataPtr + 4);
|
||||
attributes = Utility.getWord (buffer, dataPtr + 6);
|
||||
version = Utility.getWord (buffer, dataPtr + 8);
|
||||
totThreads = Utility.getLong (buffer, dataPtr + 10);
|
||||
fileSystemID = Utility.getWord (buffer, dataPtr + 14);
|
||||
separator = (char) (buffer[dataPtr + 16] & 0x00FF);
|
||||
access = Utility.getLong (buffer, dataPtr + 18);
|
||||
fileType = Utility.getLong (buffer, dataPtr + 22);
|
||||
auxType = Utility.getLong (buffer, dataPtr + 26);
|
||||
storType = Utility.getWord (buffer, dataPtr + 30);
|
||||
created = new DateTime (buffer, dataPtr + 32);
|
||||
modified = new DateTime (buffer, dataPtr + 40);
|
||||
archived = new DateTime (buffer, dataPtr + 48);
|
||||
optionSize = Utility.getWord (buffer, dataPtr + 56);
|
||||
fileNameLength = Utility.getWord (buffer, dataPtr + attributes - 2);
|
||||
|
||||
int len = attributes + fileNameLength - 6;
|
||||
byte[] crcBuffer = new byte[len + totThreads * 16];
|
||||
System.arraycopy (buffer, dataPtr + 6, crcBuffer, 0, crcBuffer.length);
|
||||
|
||||
if (crc != getCRC (crcBuffer, 0))
|
||||
{
|
||||
System.out.println ("***** Header CRC mismatch *****");
|
||||
throw new FileFormatException ("Header CRC failed");
|
||||
}
|
||||
|
||||
if (fileNameLength > 0)
|
||||
{
|
||||
int start = dataPtr + attributes;
|
||||
int end = start + fileNameLength;
|
||||
for (int i = start; i < end; i++)
|
||||
buffer[i] &= 0x7F;
|
||||
fileName = new String (buffer, start, fileNameLength);
|
||||
}
|
||||
else
|
||||
fileName = "";
|
||||
}
|
||||
|
||||
private boolean isNuFX (byte[] buffer, int ptr)
|
||||
{
|
||||
if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46
|
||||
&& buffer[ptr + 3] == (byte) 0xD8)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
StringBuilder text = new StringBuilder ();
|
||||
|
||||
text.append (String.format ("Header CRC ..... %,d (%04X)%n", crc, crc));
|
||||
text.append (String.format ("Attributes ..... %d%n", attributes));
|
||||
text.append (String.format ("Version ........ %d%n", version));
|
||||
text.append (String.format ("Threads ........ %d%n", totThreads));
|
||||
text.append (String.format ("File sys id .... %d (%s)%n", fileSystemID,
|
||||
fileSystems[fileSystemID]));
|
||||
text.append (String.format ("Separator ...... %s%n", separator));
|
||||
text.append (String.format ("Access ......... %,d%n", access));
|
||||
text.append (String.format ("File type ...... %,d%n", fileType));
|
||||
text.append (String.format ("Aux type ....... %,d%n", auxType));
|
||||
text.append (String.format ("Stor type ...... %,d%n", storType));
|
||||
text.append (String.format ("Created ........ %s%n", created.format ()));
|
||||
text.append (String.format ("Modified ....... %s%n", modified.format ()));
|
||||
text.append (String.format ("Archived ....... %s%n", archived.format ()));
|
||||
text.append (String.format ("Option size .... %,d%n", optionSize));
|
||||
text.append (String.format ("Filename len ... %,d%n", fileNameLength));
|
||||
text.append (String.format ("Filename ....... %s", fileName));
|
||||
|
||||
return text.toString ();
|
||||
}
|
||||
}
|
||||
}
|
133
src/com/bytezone/diskbrowser/utilities/Record.java
Normal file
133
src/com/bytezone/diskbrowser/utilities/Record.java
Normal file
@ -0,0 +1,133 @@
|
||||
package com.bytezone.diskbrowser.utilities;
|
||||
|
||||
// -----------------------------------------------------------------------------------//
|
||||
class Record
|
||||
// -----------------------------------------------------------------------------------//
|
||||
{
|
||||
private static String[] fileSystems =
|
||||
{ "", "ProDOS/SOS", "DOS 3.3", "DOS 3.2", "Apple II Pascal", "Macintosh HFS",
|
||||
"Macintosh MFS", "Lisa File System", "Apple CP/M", "", "MS-DOS", "High Sierra",
|
||||
"ISO 9660", "AppleShare" };
|
||||
|
||||
private final int totThreads;
|
||||
private final int crc;
|
||||
private final char separator;
|
||||
private final int fileSystemID;
|
||||
private final int attributes;
|
||||
private final int version;
|
||||
private final int access;
|
||||
private final int fileType;
|
||||
private final int auxType;
|
||||
private final int storType;
|
||||
private final DateTime created;
|
||||
private final DateTime modified;
|
||||
private final DateTime archived;
|
||||
private final int optionSize;
|
||||
private final int fileNameLength;
|
||||
private final String fileName;
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
public Record (byte[] buffer, int dataPtr) throws FileFormatException
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
// check for NuFX
|
||||
if (!isNuFX (buffer, dataPtr))
|
||||
throw new FileFormatException ("NuFX not found");
|
||||
|
||||
crc = Utility.getWord (buffer, dataPtr + 4);
|
||||
attributes = Utility.getWord (buffer, dataPtr + 6);
|
||||
version = Utility.getWord (buffer, dataPtr + 8);
|
||||
totThreads = Utility.getLong (buffer, dataPtr + 10);
|
||||
fileSystemID = Utility.getWord (buffer, dataPtr + 14);
|
||||
separator = (char) (buffer[dataPtr + 16] & 0x00FF);
|
||||
access = Utility.getLong (buffer, dataPtr + 18);
|
||||
fileType = Utility.getLong (buffer, dataPtr + 22);
|
||||
auxType = Utility.getLong (buffer, dataPtr + 26);
|
||||
storType = Utility.getWord (buffer, dataPtr + 30);
|
||||
created = new DateTime (buffer, dataPtr + 32);
|
||||
modified = new DateTime (buffer, dataPtr + 40);
|
||||
archived = new DateTime (buffer, dataPtr + 48);
|
||||
optionSize = Utility.getWord (buffer, dataPtr + 56);
|
||||
fileNameLength = Utility.getWord (buffer, dataPtr + attributes - 2);
|
||||
|
||||
int len = attributes + fileNameLength - 6;
|
||||
byte[] crcBuffer = new byte[len + totThreads * 16];
|
||||
System.arraycopy (buffer, dataPtr + 6, crcBuffer, 0, crcBuffer.length);
|
||||
|
||||
if (crc != Utility.getCRC (crcBuffer, 0))
|
||||
{
|
||||
System.out.println ("***** Header CRC mismatch *****");
|
||||
throw new FileFormatException ("Header CRC failed");
|
||||
}
|
||||
|
||||
if (fileNameLength > 0)
|
||||
{
|
||||
int start = dataPtr + attributes;
|
||||
int end = start + fileNameLength;
|
||||
for (int i = start; i < end; i++)
|
||||
buffer[i] &= 0x7F;
|
||||
fileName = new String (buffer, start, fileNameLength);
|
||||
}
|
||||
else
|
||||
fileName = "";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
private boolean isNuFX (byte[] buffer, int ptr)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
if (buffer[ptr] == 0x4E && buffer[ptr + 1] == (byte) 0xF5 && buffer[ptr + 2] == 0x46
|
||||
&& buffer[ptr + 3] == (byte) 0xD8)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
int getAttributes ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
return attributes;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
int getFileNameLength ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
return fileNameLength;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
int getTotalThreads ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
return totThreads;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
@Override
|
||||
public String toString ()
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
StringBuilder text = new StringBuilder ();
|
||||
|
||||
text.append (String.format ("Header CRC ..... %,d (%04X)%n", crc, crc));
|
||||
text.append (String.format ("Attributes ..... %d%n", attributes));
|
||||
text.append (String.format ("Version ........ %d%n", version));
|
||||
text.append (String.format ("Threads ........ %d%n", totThreads));
|
||||
text.append (String.format ("File sys id .... %d (%s)%n", fileSystemID,
|
||||
fileSystems[fileSystemID]));
|
||||
text.append (String.format ("Separator ...... %s%n", separator));
|
||||
text.append (String.format ("Access ......... %,d%n", access));
|
||||
text.append (String.format ("File type ...... %,d%n", fileType));
|
||||
text.append (String.format ("Aux type ....... %,d%n", auxType));
|
||||
text.append (String.format ("Stor type ...... %,d%n", storType));
|
||||
text.append (String.format ("Created ........ %s%n", created.format ()));
|
||||
text.append (String.format ("Modified ....... %s%n", modified.format ()));
|
||||
text.append (String.format ("Archived ....... %s%n", archived.format ()));
|
||||
text.append (String.format ("Option size .... %,d%n", optionSize));
|
||||
text.append (String.format ("Filename len ... %,d%n", fileNameLength));
|
||||
text.append (String.format ("Filename ....... %s", fileName));
|
||||
|
||||
return text.toString ();
|
||||
}
|
||||
}
|
@ -365,6 +365,24 @@ public class Utility
|
||||
return checksum.getValue ();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
protected static int getCRC (final byte[] buffer, int base)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
{
|
||||
int crc = base;
|
||||
for (int j = 0; j < buffer.length; j++)
|
||||
{
|
||||
crc = ((crc >>> 8) | (crc << 8)) & 0xFFFF;
|
||||
crc ^= (buffer[j] & 0xFF);
|
||||
crc ^= ((crc & 0xFF) >>> 4);
|
||||
crc ^= (crc << 12) & 0xFFFF;
|
||||
crc ^= ((crc & 0xFF) << 5) & 0xFFFF;
|
||||
}
|
||||
|
||||
crc &= 0xFFFF;
|
||||
return crc;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------//
|
||||
public static int crc32 (byte[] buffer, int offset, int length)
|
||||
// ---------------------------------------------------------------------------------//
|
||||
|
Loading…
Reference in New Issue
Block a user