Update j2se IO implementation to use RandomAccessFile to support seek.
This commit is contained in:
@@ -24,9 +24,11 @@ package org.luaj.lib;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.luaj.vm.LFunction;
|
||||
import org.luaj.vm.LNil;
|
||||
import org.luaj.vm.LString;
|
||||
import org.luaj.vm.LTable;
|
||||
import org.luaj.vm.LUserData;
|
||||
import org.luaj.vm.LValue;
|
||||
import org.luaj.vm.LuaState;
|
||||
|
||||
|
||||
@@ -40,10 +42,10 @@ public class IoLib extends LFunction {
|
||||
public boolean isclosed();
|
||||
/** returns new position */
|
||||
public int seek(String option, int bytecount) throws IOException;
|
||||
public byte[] readBytes(int count) throws IOException;
|
||||
public LValue readBytes(int count) throws IOException;
|
||||
public Double readNumber() throws IOException;
|
||||
public LString readLine() throws IOException;
|
||||
public LString readFile() throws IOException;
|
||||
public LValue readLine() throws IOException;
|
||||
public LValue readFile() throws IOException;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +66,7 @@ public class IoLib extends LFunction {
|
||||
|
||||
public static final String[] NAMES = {
|
||||
"io",
|
||||
"__index",
|
||||
"close",
|
||||
"flush",
|
||||
"input",
|
||||
@@ -85,51 +88,51 @@ public class IoLib extends LFunction {
|
||||
};
|
||||
|
||||
private static final int INSTALL = 0;
|
||||
private static final int IO_CLOSE = 1;
|
||||
private static final int IO_FLUSH = 2;
|
||||
private static final int IO_INPUT = 3;
|
||||
private static final int IO_LINES = 4;
|
||||
private static final int IO_OPEN = 5;
|
||||
private static final int IO_OUTPUT = 6;
|
||||
private static final int IO_POPEN = 7;
|
||||
private static final int IO_READ = 8;
|
||||
private static final int IO_TMPFILE = 9;
|
||||
private static final int IO_TYPE = 10;
|
||||
private static final int IO_WRITE = 11;
|
||||
private static final int FILE_CLOSE = 12;
|
||||
private static final int FILE_FLUSH = 13;
|
||||
private static final int FILE_LINES = 14;
|
||||
private static final int FILE_READ = 15;
|
||||
private static final int FILE_SEEK = 16;
|
||||
private static final int FILE_SETVBUF = 17;
|
||||
private static final int FILE_WRITE = 18;
|
||||
private static final int IO_INDEX = 1;
|
||||
private static final int IO_CLOSE = 2;
|
||||
private static final int IO_FLUSH = 3;
|
||||
private static final int IO_INPUT = 4;
|
||||
private static final int IO_LINES = 5;
|
||||
private static final int IO_OPEN = 6;
|
||||
private static final int IO_OUTPUT = 7;
|
||||
private static final int IO_POPEN = 8;
|
||||
private static final int IO_READ = 9;
|
||||
private static final int IO_TMPFILE = 10;
|
||||
private static final int IO_TYPE = 11;
|
||||
private static final int IO_WRITE = 12;
|
||||
private static final int FILE_CLOSE = 13;
|
||||
private static final int FILE_FLUSH = 14;
|
||||
private static final int FILE_LINES = 15;
|
||||
private static final int FILE_READ = 16;
|
||||
private static final int FILE_SEEK = 17;
|
||||
private static final int FILE_SETVBUF = 18;
|
||||
private static final int FILE_WRITE = 19;
|
||||
|
||||
private static File INPUT = null;
|
||||
private static File OUTPUT = null;
|
||||
private static File ERROR = null;
|
||||
|
||||
private static final LTable FILE_MT = new LTable();
|
||||
private static LTable FILE_MT;
|
||||
|
||||
protected void initialize( LTable globals ) {
|
||||
try {
|
||||
LTable io = new LTable();
|
||||
for ( int i=IO_CLOSE; i<=IO_WRITE; i++ )
|
||||
io.put(NAMES[i], newInstance(i));
|
||||
INPUT = openFile("-", "r");
|
||||
OUTPUT = openFile("-", "w");
|
||||
ERROR = OUTPUT;
|
||||
io.put("stdin", new LUserData(INPUT));
|
||||
io.put("stdout", new LUserData(OUTPUT));
|
||||
io.put("stderr", new LUserData(ERROR));
|
||||
for ( int i=FILE_CLOSE; i<=FILE_WRITE; i++ )
|
||||
FILE_MT.put(NAMES[i], newInstance(i));
|
||||
FILE_MT.put("__index", FILE_MT);
|
||||
globals.put( "io", io );
|
||||
PackageLib.setIsLoaded("io", io);
|
||||
} catch ( IOException ioe ) {
|
||||
throw new RuntimeException("io error: "+ioe.getMessage());
|
||||
}
|
||||
LTable io = new LTable();
|
||||
for ( int i=IO_INDEX; i<=IO_WRITE; i++ )
|
||||
io.put(NAMES[i], newInstance(i));
|
||||
io.luaSetMetatable(io);
|
||||
FILE_MT = new LTable();
|
||||
for ( int i=FILE_CLOSE; i<=FILE_WRITE; i++ )
|
||||
FILE_MT.put(NAMES[i], newInstance(i));
|
||||
FILE_MT.put("__index", FILE_MT);
|
||||
INPUT = null;
|
||||
OUTPUT = null;
|
||||
ERROR = null;
|
||||
globals.put( "io", io );
|
||||
PackageLib.setIsLoaded("io", io);
|
||||
}
|
||||
|
||||
private static final char DEFVAL_NONE = 0;
|
||||
private static final char DEFVAL_STDIN = 'r';
|
||||
private static final char DEFVAL_STDOUT= 'w';
|
||||
|
||||
private final int id;
|
||||
|
||||
@@ -141,14 +144,46 @@ public class IoLib extends LFunction {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LString luaAsString() {
|
||||
return new LString(toJavaString());
|
||||
}
|
||||
|
||||
public String toJavaString() {
|
||||
return "io."+toString();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return NAMES[id]+"()";
|
||||
}
|
||||
|
||||
|
||||
private File input(LuaState vm) {
|
||||
return INPUT!=null? INPUT: (INPUT=ioopenfile(vm,"-","r"));
|
||||
}
|
||||
|
||||
private File output(LuaState vm) {
|
||||
return OUTPUT!=null? OUTPUT: (OUTPUT=ioopenfile(vm,"-","w"));
|
||||
}
|
||||
|
||||
private File error(LuaState vm) {
|
||||
return ERROR!=null? ERROR: (ERROR=ioopenfile(vm,"-","w"));
|
||||
}
|
||||
|
||||
public LValue __index(LuaState vm, LValue table, LValue key) {
|
||||
String k = key.toJavaString();
|
||||
if ( "stdout".equals(k) )
|
||||
return touserdata(output(vm));
|
||||
else if ( "stdin".equals(k) )
|
||||
return touserdata(input(vm));
|
||||
else if ( "stderr".equals(k) )
|
||||
return touserdata(error(vm));
|
||||
else
|
||||
return LNil.NIL;
|
||||
}
|
||||
|
||||
public boolean luaStackCall( LuaState vm ) {
|
||||
File f;
|
||||
String s;
|
||||
int i,n;
|
||||
int n;
|
||||
try {
|
||||
switch ( id ) {
|
||||
/* Load the table library dynamically */
|
||||
@@ -156,12 +191,12 @@ public class IoLib extends LFunction {
|
||||
initialize(vm._G);
|
||||
break;
|
||||
case IO_CLOSE:
|
||||
optfile(vm, 2, OUTPUT).close();
|
||||
optfile(vm,2,DEFVAL_STDOUT).close();
|
||||
vm.resettop();
|
||||
vm.pushboolean(true);
|
||||
break;
|
||||
case IO_FLUSH:
|
||||
checkopen(vm,OUTPUT);
|
||||
checkopen(vm,output(vm));
|
||||
OUTPUT.flush();
|
||||
vm.resettop();
|
||||
vm.pushboolean(true);
|
||||
@@ -169,10 +204,15 @@ public class IoLib extends LFunction {
|
||||
case IO_INPUT:
|
||||
INPUT = ((s = vm.optstring(1, null)) != null)?
|
||||
ioopenfile(vm,s,"r"):
|
||||
optfile(vm,1,INPUT);
|
||||
optfile(vm,2,DEFVAL_STDIN);
|
||||
setresult(vm, INPUT);
|
||||
break;
|
||||
case IO_LINES:
|
||||
INPUT = ((s = vm.optstring(1, null)) != null)?
|
||||
ioopenfile(vm,s,"r"):
|
||||
optfile(vm,2,DEFVAL_STDIN);
|
||||
vm.resettop();
|
||||
vm.pushlvalue(lines(INPUT));
|
||||
break;
|
||||
case IO_OPEN:
|
||||
setresult(vm, openFile(vm.checkstring(2), vm.optstring(3,"r")));
|
||||
@@ -180,7 +220,7 @@ public class IoLib extends LFunction {
|
||||
case IO_OUTPUT:
|
||||
OUTPUT = ((s = vm.optstring(2, null)) != null)?
|
||||
ioopenfile(vm,s,"w"):
|
||||
optfile(vm,1,OUTPUT);
|
||||
optfile(vm,2,DEFVAL_STDOUT);
|
||||
setresult(vm, OUTPUT);
|
||||
break;
|
||||
case IO_POPEN:
|
||||
@@ -191,7 +231,7 @@ public class IoLib extends LFunction {
|
||||
case IO_TMPFILE:
|
||||
break;
|
||||
case IO_TYPE:
|
||||
f = optfile(vm,2,null);
|
||||
f = optfile(vm,2,DEFVAL_NONE);
|
||||
vm.resettop();
|
||||
if ( f != null )
|
||||
vm.pushstring(f.isclosed()? "closed file": "file");
|
||||
@@ -212,6 +252,9 @@ public class IoLib extends LFunction {
|
||||
vm.pushboolean(true);
|
||||
break;
|
||||
case FILE_LINES:
|
||||
f = checkfile(vm,2);
|
||||
vm.resettop();
|
||||
vm.pushlvalue(lines(f));
|
||||
break;
|
||||
case FILE_READ:
|
||||
f = checkfile(vm,2);
|
||||
@@ -224,6 +267,7 @@ public class IoLib extends LFunction {
|
||||
vm.pushinteger(n);
|
||||
break;
|
||||
case FILE_SETVBUF:
|
||||
vm.resettop();
|
||||
break;
|
||||
case FILE_WRITE:
|
||||
f = checkfile(vm,2);
|
||||
@@ -241,6 +285,21 @@ public class IoLib extends LFunction {
|
||||
return false;
|
||||
}
|
||||
|
||||
private LValue lines(final File f) {
|
||||
return new LFunction() {
|
||||
public boolean luaStackCall(LuaState vm) {
|
||||
vm.resettop();
|
||||
try {
|
||||
vm.pushlvalue(f.readLine());
|
||||
} catch (IOException e) {
|
||||
vm.pushnil();
|
||||
vm.pushstring("io error: "+e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void iowrite(LuaState vm, File f) throws IOException {
|
||||
checkopen(vm,f);
|
||||
for ( int i=2, n=vm.gettop(); i<=n; i++ )
|
||||
@@ -254,15 +313,15 @@ public class IoLib extends LFunction {
|
||||
int i,n=vm.gettop();
|
||||
for ( i=2; i<=n; i++ ) {
|
||||
if ( vm.isnumber(i) ) {
|
||||
vm.pushlstring(f.readBytes(vm.tointeger(i)));
|
||||
vm.pushlvalue(f.readBytes(vm.tointeger(i)));
|
||||
} else {
|
||||
String format = vm.checkstring(i);
|
||||
if ( "*n".equals(format) )
|
||||
vm.pushnumber(f.readNumber());
|
||||
else if ( "*a".equals(format) )
|
||||
vm.pushlstring(f.readFile());
|
||||
vm.pushlvalue(f.readFile());
|
||||
else if ( "*l".equals(format) )
|
||||
vm.pushlstring(f.readLine());
|
||||
vm.pushlvalue(f.readLine());
|
||||
else
|
||||
vm.typerror( i, "(invalid format)" );
|
||||
}
|
||||
@@ -275,9 +334,12 @@ public class IoLib extends LFunction {
|
||||
return (File) vm.checkudata(index, File.class);
|
||||
}
|
||||
|
||||
private static File optfile(LuaState vm, int index, File defval) {
|
||||
private File optfile(LuaState vm, int index, char defval) {
|
||||
Object u = vm.touserdata(index);
|
||||
return (u instanceof File? (File) u: defval);
|
||||
return (u instanceof File? (File) u:
|
||||
defval==DEFVAL_STDOUT? output(vm):
|
||||
defval==DEFVAL_STDIN? input(vm):
|
||||
null);
|
||||
}
|
||||
|
||||
private static void checkopen(LuaState vm, File file) {
|
||||
@@ -287,13 +349,16 @@ public class IoLib extends LFunction {
|
||||
|
||||
private static void setresult(LuaState vm, File file) {
|
||||
vm.settop(0);
|
||||
vm.pushlvalue(new LUserData(file, FILE_MT));
|
||||
vm.pushlvalue(touserdata(file));
|
||||
}
|
||||
|
||||
private static LUserData touserdata(File file) {
|
||||
return new LUserData(file, FILE_MT);
|
||||
}
|
||||
|
||||
private File ioopenfile(LuaState vm, String filename, String mode) {
|
||||
try {
|
||||
File f = openFile( filename, mode );
|
||||
setresult( vm, f );
|
||||
return f;
|
||||
} catch ( Exception e ) {
|
||||
vm.error("io error: "+e.getMessage());
|
||||
|
||||
@@ -22,16 +22,22 @@
|
||||
package org.luaj.lib.j2se;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInput;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
import org.luaj.lib.BaseLib;
|
||||
import org.luaj.lib.IoLib;
|
||||
import org.luaj.vm.LNil;
|
||||
import org.luaj.vm.LString;
|
||||
import org.luaj.vm.LTable;
|
||||
import org.luaj.vm.LValue;
|
||||
|
||||
|
||||
public class J2seIoLib extends IoLib {
|
||||
@@ -53,143 +59,153 @@ public class J2seIoLib extends IoLib {
|
||||
}
|
||||
|
||||
protected File openFile(String filename, String mode) throws IOException {
|
||||
return ( mode == null || mode.startsWith("r") )?
|
||||
new InputFileImpl(
|
||||
"-".equals(filename)?
|
||||
System.in:
|
||||
new FileInputStream(filename) ):
|
||||
new OutputFileImpl(
|
||||
"-".equals(filename)?
|
||||
System.out:
|
||||
new FileOutputStream(filename, mode.endsWith("+")) );
|
||||
boolean isstdfile = "-".equals(filename);
|
||||
boolean isreadmode = mode.startsWith("r");
|
||||
if ( isstdfile )
|
||||
return isreadmode?
|
||||
new FileImpl(BaseLib.STDIN != null? BaseLib.STDIN: System.in):
|
||||
new FileImpl(BaseLib.STDOUT != null? BaseLib.STDOUT: System.out);
|
||||
boolean isappend = mode.startsWith("a");
|
||||
// TODO: handle update mode
|
||||
// boolean isupdate = mode.endsWith("+");
|
||||
RandomAccessFile f = new RandomAccessFile(filename,isreadmode? "r": "rw");
|
||||
if ( isappend )
|
||||
f.seek(f.length());
|
||||
return new FileImpl( f );
|
||||
}
|
||||
|
||||
private static void notimplemented() {
|
||||
throw new RuntimeException("not implemented");
|
||||
}
|
||||
|
||||
private static final class InputFileImpl implements File {
|
||||
private static final class FileImpl implements File {
|
||||
private final RandomAccessFile file;
|
||||
private final InputStream is;
|
||||
private final OutputStream os;
|
||||
private final Closeable closer;
|
||||
private final DataInput din;
|
||||
private final DataOutput dout;
|
||||
private boolean closed = false;
|
||||
private InputFileImpl( InputStream is ) {
|
||||
this.is = is.markSupported()? is: new BufferedInputStream(is);
|
||||
private FileImpl( RandomAccessFile file, InputStream is, OutputStream os, DataInput din, DataOutput dout, Closeable closer ) {
|
||||
this.file = file;
|
||||
this.is = is!=null? is.markSupported()? is: new BufferedInputStream(is): null;
|
||||
this.os = os;
|
||||
this.din = din;
|
||||
this.dout = dout;
|
||||
this.closer = closer;
|
||||
}
|
||||
private FileImpl( RandomAccessFile f ) {
|
||||
this( f, null, null, f, f, f );
|
||||
}
|
||||
private FileImpl( InputStream i ) {
|
||||
this( null, i, null, new DataInputStream(i), null, i );
|
||||
}
|
||||
private FileImpl( OutputStream o ) {
|
||||
this( null, null, o, null, new DataOutputStream(o), o );
|
||||
}
|
||||
public String toString() {
|
||||
return "file ("+this.hashCode()+")";
|
||||
}
|
||||
public void close() throws IOException {
|
||||
closed = true;
|
||||
is.close();
|
||||
closer.close();
|
||||
}
|
||||
public void flush() {
|
||||
notimplemented();
|
||||
public void flush() throws IOException {
|
||||
if ( os != null )
|
||||
os.flush();
|
||||
}
|
||||
public void write(LString string) {
|
||||
notimplemented();
|
||||
public void write(LString s) throws IOException {
|
||||
if ( dout != null )
|
||||
dout.write( s.m_bytes, s.m_offset, s.m_length );
|
||||
else
|
||||
notimplemented();
|
||||
}
|
||||
public boolean isclosed() {
|
||||
return closed;
|
||||
}
|
||||
public int seek(String option, int bytecount) throws IOException {
|
||||
public int seek(String option, int pos) throws IOException {
|
||||
if ( file != null ) {
|
||||
if ( "set".equals(option) )
|
||||
file.seek(pos);
|
||||
else if ( "end".equals(option) )
|
||||
file.seek(file.length()+pos);
|
||||
else
|
||||
file.seek(file.getFilePointer()+pos);
|
||||
return (int) file.getFilePointer();
|
||||
}
|
||||
notimplemented();
|
||||
return 0;
|
||||
}
|
||||
public byte[] readBytes(int count) throws IOException {
|
||||
byte[] b = new byte[count];
|
||||
int n;
|
||||
for ( int i=0; i<count; ) {
|
||||
n = is.read(b,i,count-i);
|
||||
if ( n < 0 )
|
||||
throw new java.io.EOFException("eof");
|
||||
i += n;
|
||||
public LValue readBytes(int count) throws IOException {
|
||||
if ( din != null ) {
|
||||
byte[] b = new byte[count];
|
||||
din.readFully(b);
|
||||
return new LString(b);
|
||||
}
|
||||
return b;
|
||||
notimplemented();
|
||||
return LNil.NIL;
|
||||
}
|
||||
public LString readLine() throws IOException {
|
||||
public LValue readLine() throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int c;
|
||||
while ( true ) {
|
||||
int c = is.read();
|
||||
if ( is != null ) {
|
||||
c = is.read();
|
||||
} else {
|
||||
c = file.read();
|
||||
}
|
||||
if ( c < 0 || c == '\n' )
|
||||
break;
|
||||
baos.write(c);
|
||||
}
|
||||
return new LString(baos.toByteArray());
|
||||
return ( c < 0 && baos.size() == 0 )?
|
||||
LNil.NIL:
|
||||
new LString(baos.toByteArray());
|
||||
}
|
||||
public LString readFile() throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
while ( true ) {
|
||||
int c = is.read();
|
||||
if ( c < 0 )
|
||||
break;
|
||||
baos.write(c);
|
||||
}
|
||||
return new LString(baos.toByteArray());
|
||||
public LValue readFile() throws IOException {
|
||||
if ( file != null ) {
|
||||
return readBytes((int) (file.length() - file.getFilePointer()));
|
||||
}
|
||||
notimplemented();
|
||||
return null;
|
||||
}
|
||||
public Double readNumber() throws IOException {
|
||||
if ( is == null && file == null )
|
||||
notimplemented();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
readChars(" \t\r\n",null);
|
||||
readChars("-+",baos);
|
||||
//readChars("0",baos);
|
||||
//readChars("xX",baos);
|
||||
readChars("0123456789",baos);
|
||||
readChars(".",baos);
|
||||
readChars("0123456789",baos);
|
||||
//readChars("eEfFgG",baos);
|
||||
// readChars("+-",baos);
|
||||
//readChars("0123456789",baos);
|
||||
String s = baos.toString();
|
||||
return s.length()>0? Double.valueOf(s): null;
|
||||
}
|
||||
private void readChars(String chars, ByteArrayOutputStream baos) throws IOException {
|
||||
int c;
|
||||
while ( true ) {
|
||||
c = is.read();
|
||||
if ( c < 0 )
|
||||
return null;
|
||||
if ( "\t\r\n ".indexOf(c) < 0 )
|
||||
break;
|
||||
}
|
||||
if ( (c < '0' || c > '9') && c != '-' && c != '.' )
|
||||
return null;
|
||||
baos.write(c);
|
||||
while ( true ) {
|
||||
is.mark(1);
|
||||
c = is.read();
|
||||
if ( c < 0 )
|
||||
break;
|
||||
if ( (c < '0' || c > '9') && c != '-' && c != '.' ) {
|
||||
is.reset();
|
||||
break;
|
||||
if ( is != null ) {
|
||||
is.mark(1);
|
||||
c = is.read();
|
||||
} else {
|
||||
baos.write( c );
|
||||
c = file.read();
|
||||
}
|
||||
if ( chars.indexOf(c) < 0 ) {
|
||||
if ( is != null )
|
||||
is.reset();
|
||||
else if ( file != null )
|
||||
file.seek(file.getFilePointer()-1);
|
||||
return;
|
||||
}
|
||||
if ( baos != null )
|
||||
baos.write( c );
|
||||
}
|
||||
return Double.valueOf(baos.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OutputFileImpl implements File {
|
||||
private final OutputStream os;
|
||||
private boolean closed = false;
|
||||
private OutputFileImpl( OutputStream os ) {
|
||||
this.os = os;
|
||||
}
|
||||
public void close() throws IOException {
|
||||
closed = true;
|
||||
os.close();
|
||||
}
|
||||
public void flush() throws IOException {
|
||||
os.flush();
|
||||
}
|
||||
public void write(LString s) throws IOException {
|
||||
os.write(s.m_bytes, s.m_offset, s.m_length);
|
||||
}
|
||||
public boolean isclosed() {
|
||||
return closed;
|
||||
}
|
||||
public int seek(String option, int bytecount) throws IOException {
|
||||
notimplemented();
|
||||
return 0;
|
||||
}
|
||||
public byte[] readBytes(int count) throws IOException {
|
||||
notimplemented();
|
||||
return null;
|
||||
}
|
||||
public LString readLine() throws IOException {
|
||||
notimplemented();
|
||||
return null;
|
||||
}
|
||||
public Double readNumber() throws IOException {
|
||||
notimplemented();
|
||||
return null;
|
||||
}
|
||||
public LString readFile() throws IOException {
|
||||
notimplemented();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- simple io-library tests
|
||||
print( io ~= nil )
|
||||
print( io.open ~= nil )
|
||||
print( io.stdin ~= nil )
|
||||
print( io.stdout ~= nil )
|
||||
print( io.stderr ~= nil )
|
||||
|
||||
Reference in New Issue
Block a user