build samples without debug info, fix try/catch processing
This commit is contained in:
@@ -19,6 +19,7 @@ import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.InsnWrapArg;
|
||||
import jadx.core.dex.instructions.args.LiteralArg;
|
||||
import jadx.core.dex.instructions.args.NamedArg;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.mods.ConstructorInsn;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
@@ -77,10 +78,14 @@ public class InsnGen {
|
||||
return arg((RegisterArg) arg);
|
||||
} else if (arg.isLiteral()) {
|
||||
return lit((LiteralArg) arg);
|
||||
} else {
|
||||
} else if (arg.isInsnWrap()) {
|
||||
CodeWriter code = new CodeWriter();
|
||||
makeInsn(((InsnWrapArg) arg).getWrapInsn(), code, true);
|
||||
return code.toString();
|
||||
} else if (arg.isNamed()) {
|
||||
return ((NamedArg) arg).getName();
|
||||
} else {
|
||||
throw new CodegenException("Unknown arg type " + arg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +323,7 @@ public class InsnGen {
|
||||
|
||||
case MONITOR_EXIT:
|
||||
if (isFallback()) {
|
||||
code.add("monitor-exit(").add(arg(insn.getArg(0))).add(')');
|
||||
code.add("monitor-exit(").add(arg(insn, 0)).add(')');
|
||||
} else {
|
||||
state.add(InsnGenState.SKIP);
|
||||
}
|
||||
@@ -328,11 +333,7 @@ public class InsnGen {
|
||||
if (isFallback()) {
|
||||
code.add("move-exception");
|
||||
} else {
|
||||
// don't have body
|
||||
if (state.contains(InsnGenState.BODY_ONLY))
|
||||
code.add(arg(insn.getResult()));
|
||||
else
|
||||
state.add(InsnGenState.SKIP);
|
||||
code.add(arg(insn, 0));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -340,7 +341,7 @@ public class InsnGen {
|
||||
break;
|
||||
|
||||
case ARGS:
|
||||
code.add(arg(insn.getArg(0)));
|
||||
code.add(arg(insn, 0));
|
||||
break;
|
||||
|
||||
case NOP:
|
||||
|
||||
@@ -8,6 +8,7 @@ import jadx.core.dex.attributes.JadxErrorAttr;
|
||||
import jadx.core.dex.attributes.annotations.MethodParameters;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.NamedArg;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
@@ -118,7 +119,7 @@ public class MethodGen {
|
||||
(MethodParameters) mth.getAttributes().get(AttributeType.ANNOTATION_MTH_PARAMETERS);
|
||||
|
||||
int i = 0;
|
||||
for (Iterator<RegisterArg> it = args.iterator(); it.hasNext();) {
|
||||
for (Iterator<RegisterArg> it = args.iterator(); it.hasNext(); ) {
|
||||
RegisterArg arg = it.next();
|
||||
|
||||
// add argument annotation
|
||||
@@ -179,16 +180,12 @@ public class MethodGen {
|
||||
/**
|
||||
* Put variable declaration and return variable name (used for assignments)
|
||||
*
|
||||
* @param arg
|
||||
* register variable
|
||||
* @param arg register variable
|
||||
* @return variable name
|
||||
*/
|
||||
public String assignArg(RegisterArg arg) {
|
||||
String name = makeArgName(arg);
|
||||
if (varNames.add(name))
|
||||
return name;
|
||||
|
||||
if (fallback)
|
||||
if (varNames.add(name) || fallback)
|
||||
return name;
|
||||
|
||||
name = getUniqVarName(name);
|
||||
@@ -196,6 +193,16 @@ public class MethodGen {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String assignNamedArg(NamedArg arg) {
|
||||
String name = arg.getName();
|
||||
if (varNames.add(name) || fallback)
|
||||
return name;
|
||||
|
||||
name = getUniqVarName(name);
|
||||
arg.setName(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getUniqVarName(String name) {
|
||||
String r;
|
||||
int i = 2;
|
||||
|
||||
@@ -270,7 +270,7 @@ public class RegionGen extends InsnGen {
|
||||
code.startLine("} catch (");
|
||||
code.add(handler.isCatchAll() ? "Throwable" : useClass(handler.getCatchType()));
|
||||
code.add(' ');
|
||||
code.add(mgen.assignArg(handler.getArg()));
|
||||
code.add(mgen.assignNamedArg(handler.getArg()));
|
||||
code.add(") {");
|
||||
makeRegionIndent(code, region);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ public final class BlockRegState {
|
||||
regType = new TypedVar(arg.getType());
|
||||
regs[arg.getRegNum()].setTypedVar(regType);
|
||||
}
|
||||
arg.replace(regType);
|
||||
regType.getUseList().add(arg);
|
||||
regType.use(arg);
|
||||
}
|
||||
|
||||
public RegisterArg getRegister(int r) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
import jadx.core.dex.instructions.args.NamedArg;
|
||||
import jadx.core.dex.instructions.args.PrimitiveType;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.nodes.DexNode;
|
||||
@@ -387,7 +388,8 @@ public class InsnDecoder {
|
||||
|
||||
case Opcodes.MOVE_EXCEPTION:
|
||||
return insn(InsnType.MOVE_EXCEPTION,
|
||||
InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT)));
|
||||
InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT)),
|
||||
new NamedArg("e", ArgType.unknown(PrimitiveType.OBJECT)));
|
||||
|
||||
case Opcodes.RETURN_VOID:
|
||||
return new InsnNode(InsnType.RETURN, 0);
|
||||
@@ -668,13 +670,27 @@ public class InsnDecoder {
|
||||
return inode;
|
||||
}
|
||||
|
||||
private InsnNode insn(InsnType type, RegisterArg res) {
|
||||
InsnNode node = new InsnNode(type, 0);
|
||||
node.setResult(res);
|
||||
return node;
|
||||
}
|
||||
|
||||
private InsnNode insn(InsnType type, RegisterArg res, InsnArg arg) {
|
||||
InsnNode node = new InsnNode(type, 1);
|
||||
node.setResult(res);
|
||||
node.addArg(arg);
|
||||
return node;
|
||||
}
|
||||
|
||||
private InsnNode insn(InsnType type, RegisterArg res, InsnArg... args) {
|
||||
InsnNode inode = new InsnNode(type, args == null ? 0 : args.length);
|
||||
inode.setResult(res);
|
||||
if (args != null)
|
||||
InsnNode node = new InsnNode(type, args == null ? 0 : args.length);
|
||||
node.setResult(res);
|
||||
if (args != null) {
|
||||
for (InsnArg arg : args)
|
||||
inode.addArg(arg);
|
||||
return inode;
|
||||
node.addArg(arg);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private int getMoveResultRegister(DecodedInstruction[] insnArr, int offset) {
|
||||
|
||||
@@ -344,6 +344,11 @@ public abstract class ArgType {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(a.isGenericType())
|
||||
return a;
|
||||
if(b.isGenericType())
|
||||
return b;
|
||||
|
||||
if (a.isObject() && b.isObject()) {
|
||||
if (a.getObject().equals(b.getObject())) {
|
||||
if (a.getGenericTypes() != null)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package jadx.core.dex.instructions.args;
|
||||
|
||||
public class ImmutableTypedVar extends TypedVar {
|
||||
|
||||
public ImmutableTypedVar(ArgType initType) {
|
||||
super(initType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forceSetType(ArgType newType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean merge(TypedVar typedVar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean merge(ArgType mtype) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ public abstract class InsnArg extends Typed {
|
||||
protected InsnNode parentInsn;
|
||||
|
||||
public static RegisterArg reg(int regNum, ArgType type) {
|
||||
assert regNum >= 0 : "Register number must be positive";
|
||||
return new RegisterArg(regNum, type);
|
||||
}
|
||||
|
||||
@@ -46,6 +45,10 @@ public abstract class InsnArg extends Typed {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isNamed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public InsnNode getParentInsn() {
|
||||
return parentInsn;
|
||||
}
|
||||
@@ -75,5 +78,4 @@ public abstract class InsnArg extends Typed {
|
||||
public int getRegNum() {
|
||||
throw new UnsupportedOperationException("Must be called from RegisterArg");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package jadx.core.dex.instructions.args;
|
||||
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
|
||||
public class InsnWrapArg extends InsnArg {
|
||||
public final class InsnWrapArg extends InsnArg {
|
||||
|
||||
private final InsnNode wrappedInsn;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package jadx.core.dex.instructions.args;
|
||||
import jadx.core.codegen.TypeGen;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
public class LiteralArg extends InsnArg {
|
||||
public final class LiteralArg extends InsnArg {
|
||||
|
||||
private final long literal;
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package jadx.core.dex.instructions.args;
|
||||
|
||||
public final class NamedArg extends InsnArg {
|
||||
|
||||
private String name;
|
||||
|
||||
public NamedArg(String name, ArgType type) {
|
||||
this.name = name;
|
||||
this.typedVar = new TypedVar(type);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNamed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(" + name + " " + typedVar + ")";
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ public abstract class Typed {
|
||||
}
|
||||
|
||||
public void replace(TypedVar newVar) {
|
||||
assert newVar != null;
|
||||
if (typedVar == newVar)
|
||||
return;
|
||||
|
||||
|
||||
@@ -43,6 +43,11 @@ public class TypedVar {
|
||||
}
|
||||
}
|
||||
|
||||
public void use(InsnArg arg) {
|
||||
arg.replace(this);
|
||||
useList.add(arg);
|
||||
}
|
||||
|
||||
public List<InsnArg> getUseList() {
|
||||
return useList;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import jadx.core.utils.exceptions.DecodeException;
|
||||
import jadx.core.utils.files.InputFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -99,7 +100,7 @@ public class DexNode {
|
||||
for (short t : paramList.getTypes()) {
|
||||
args.add(getType(t));
|
||||
}
|
||||
return args;
|
||||
return Collections.unmodifiableList(args);
|
||||
}
|
||||
|
||||
public Code readCode(Method mth) {
|
||||
|
||||
@@ -169,23 +169,24 @@ public class MethodNode extends LineAttrNode implements ILoadable {
|
||||
if (argsTypes == null)
|
||||
return false;
|
||||
|
||||
if (argsTypes.size() != mthInfo.getArgumentsTypes().size()) {
|
||||
List<ArgType> mthArgs = mthInfo.getArgumentsTypes();
|
||||
if (argsTypes.size() != mthArgs.size()) {
|
||||
if (argsTypes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!mthInfo.isConstructor()) {
|
||||
LOG.warn("Wrong signature parse result: " + sign + " -> " + argsTypes
|
||||
+ ", not generic version: " + mthInfo.getArgumentsTypes());
|
||||
+ ", not generic version: " + mthArgs);
|
||||
return false;
|
||||
} else if (getParentClass().getAccessFlags().isEnum()) {
|
||||
// TODO:
|
||||
argsTypes.add(0, mthInfo.getArgumentsTypes().get(0));
|
||||
argsTypes.add(1, mthInfo.getArgumentsTypes().get(1));
|
||||
argsTypes.add(0, mthArgs.get(0));
|
||||
argsTypes.add(1, mthArgs.get(1));
|
||||
} else {
|
||||
// add synthetic arg for outer class
|
||||
argsTypes.add(0, mthInfo.getArgumentsTypes().get(0));
|
||||
argsTypes.add(0, mthArgs.get(0));
|
||||
}
|
||||
if (argsTypes.size() != mthInfo.getArgumentsTypes().size()) {
|
||||
if (argsTypes.size() != mthArgs.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -454,6 +455,11 @@ public class MethodNode extends LineAttrNode implements ILoadable {
|
||||
return exceptionHandlers;
|
||||
}
|
||||
|
||||
public boolean isMethodOverloaded() {
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getRegsCount() {
|
||||
return regsCount;
|
||||
}
|
||||
@@ -505,5 +511,4 @@ public class MethodNode extends LineAttrNode implements ILoadable {
|
||||
+ " " + parentClass.getFullName() + "." + mthInfo.getName()
|
||||
+ "(" + Utils.listToString(mthInfo.getArgumentsTypes()) + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package jadx.core.dex.trycatch;
|
||||
|
||||
import jadx.core.Consts;
|
||||
import jadx.core.dex.info.ClassInfo;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.instructions.args.NamedArg;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.IContainer;
|
||||
import jadx.core.utils.InsnUtils;
|
||||
@@ -18,7 +18,7 @@ public class ExceptionHandler {
|
||||
private BlockNode handleBlock;
|
||||
private final List<BlockNode> blocks = new ArrayList<BlockNode>();
|
||||
private IContainer handlerRegion;
|
||||
private RegisterArg arg;
|
||||
private NamedArg arg;
|
||||
|
||||
private TryCatchBlock tryBlock;
|
||||
|
||||
@@ -63,11 +63,11 @@ public class ExceptionHandler {
|
||||
this.handlerRegion = handlerRegion;
|
||||
}
|
||||
|
||||
public RegisterArg getArg() {
|
||||
public NamedArg getArg() {
|
||||
return arg;
|
||||
}
|
||||
|
||||
public void setArg(RegisterArg arg) {
|
||||
public void setArg(NamedArg arg) {
|
||||
this.arg = arg;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package jadx.core.dex.visitors;
|
||||
import jadx.core.dex.attributes.AttributeType;
|
||||
import jadx.core.dex.instructions.InsnType;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.instructions.args.NamedArg;
|
||||
import jadx.core.dex.instructions.args.RegisterArg;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
@@ -40,15 +41,22 @@ public class BlockProcessingHelper {
|
||||
if (!block.getInstructions().isEmpty()) {
|
||||
InsnNode me = block.getInstructions().get(0);
|
||||
ExcHandlerAttr handlerAttr = (ExcHandlerAttr) me.getAttributes().get(AttributeType.EXC_HANDLER);
|
||||
if (handlerAttr != null) {
|
||||
if (handlerAttr != null && me.getType() == InsnType.MOVE_EXCEPTION) {
|
||||
ExceptionHandler excHandler = handlerAttr.getHandler();
|
||||
assert me.getType() == InsnType.MOVE_EXCEPTION && me.getOffset() == excHandler.getHandleOffset();
|
||||
assert me.getOffset() == excHandler.getHandleOffset();
|
||||
// set correct type for 'move-exception' operation
|
||||
RegisterArg excArg = me.getResult();
|
||||
if (excHandler.isCatchAll())
|
||||
excArg.getTypedVar().forceSetType(ArgType.THROWABLE);
|
||||
else
|
||||
excArg.getTypedVar().forceSetType(excHandler.getCatchType().getType());
|
||||
RegisterArg resArg = me.getResult();
|
||||
NamedArg excArg = (NamedArg) me.getArg(0);
|
||||
ArgType type;
|
||||
if (excHandler.isCatchAll()) {
|
||||
type = ArgType.THROWABLE;
|
||||
excArg.setName("th");
|
||||
} else {
|
||||
type = excHandler.getCatchType().getType();
|
||||
excArg.setName("e");
|
||||
}
|
||||
resArg.getTypedVar().forceSetType(type);
|
||||
excArg.getTypedVar().forceSetType(type);
|
||||
|
||||
excHandler.setArg(excArg);
|
||||
block.getAttributes().add(handlerAttr);
|
||||
|
||||
@@ -70,18 +70,20 @@ public class CodeShrinker extends AbstractVisitor {
|
||||
} else {
|
||||
// TODO implement rules for shrink insn from different blocks
|
||||
BlockNode useBlock = BlockUtils.getBlockByInsn(mth, useInsn);
|
||||
if (useBlock != null && useBlock.getPredecessors().contains(block)) {
|
||||
if (useBlock != null
|
||||
&& (useBlock.getPredecessors().contains(block)
|
||||
|| insn.getType() == InsnType.MOVE_EXCEPTION)) {
|
||||
wrap = true;
|
||||
}
|
||||
}
|
||||
if (wrap) {
|
||||
if (useInsn.getType() == InsnType.MOVE) {
|
||||
// TODO
|
||||
// remover.add(useInsn);
|
||||
} else {
|
||||
// if (useInsn.getType() == InsnType.MOVE) {
|
||||
// // TODO
|
||||
// // remover.add(useInsn);
|
||||
// } else {
|
||||
useInsnArg.wrapInstruction(insn);
|
||||
remover.add(insn);
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +195,7 @@ public class CodeShrinker extends AbstractVisitor {
|
||||
while (i.isInsnWrap()) {
|
||||
InsnNode wrapInsn = ((InsnWrapArg) i).getWrapInsn();
|
||||
chain.add(wrapInsn);
|
||||
if(wrapInsn.getArgsCount() == 0)
|
||||
if (wrapInsn.getArgsCount() == 0)
|
||||
break;
|
||||
|
||||
i = wrapInsn.getArg(0);
|
||||
|
||||
@@ -19,7 +19,6 @@ import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.trycatch.ExcHandlerAttr;
|
||||
import jadx.core.dex.trycatch.ExceptionHandler;
|
||||
import jadx.core.dex.trycatch.TryCatchBlock;
|
||||
import jadx.core.utils.BlockUtils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
@@ -188,45 +187,48 @@ public class ModVisitor extends AbstractVisitor {
|
||||
if (handlerAttr == null)
|
||||
return;
|
||||
|
||||
TryCatchBlock tryBlock = handlerAttr.getTryBlock();
|
||||
ExceptionHandler excHandler = handlerAttr.getHandler();
|
||||
List<InsnNode> blockInsns = block.getInstructions();
|
||||
int size = blockInsns.size();
|
||||
if (size > 0 && blockInsns.get(0).getType() == InsnType.MOVE_EXCEPTION) {
|
||||
InstructionRemover.remove(block, 0);
|
||||
}
|
||||
|
||||
int totalSize = 0;
|
||||
boolean noExitNode = true; // check if handler has exit edge to block not from this handler
|
||||
for (BlockNode excBlock : excHandler.getBlocks()) {
|
||||
List<InsnNode> insns = excBlock.getInstructions();
|
||||
size = insns.size();
|
||||
if (noExitNode)
|
||||
if (noExitNode) {
|
||||
noExitNode = excHandler.getBlocks().containsAll(excBlock.getCleanSuccessors());
|
||||
}
|
||||
|
||||
List<InsnNode> insns = excBlock.getInstructions();
|
||||
int size = insns.size();
|
||||
if (excHandler.isCatchAll()
|
||||
&& size > 0
|
||||
&& insns.get(size - 1).getType() == InsnType.THROW) {
|
||||
|
||||
InstructionRemover.remove(excBlock, size - 1);
|
||||
size = insns.size();
|
||||
|
||||
// move not removed instructions to 'finally' block
|
||||
if (size != 0) {
|
||||
if (insns.size() != 0) {
|
||||
// TODO: support instructions from several blocks
|
||||
// tryBlock.setFinalBlockFromInsns(mth, insns);
|
||||
|
||||
// TODO; because of incomplete realization don't extract final block,
|
||||
// TODO: because of incomplete realization don't extract final block,
|
||||
// just remove unnecessary instructions
|
||||
insns.clear();
|
||||
|
||||
size = insns.size();
|
||||
}
|
||||
}
|
||||
totalSize += size;
|
||||
}
|
||||
if (totalSize == 0 && noExitNode)
|
||||
tryBlock.removeHandler(mth, excHandler);
|
||||
|
||||
List<InsnNode> blockInsns = block.getInstructions();
|
||||
if (blockInsns.size() > 0) {
|
||||
InsnNode insn = blockInsns.get(0);
|
||||
if (insn.getType() == InsnType.MOVE_EXCEPTION
|
||||
&& insn.getResult().getTypedVar().getUseList().size() <= 1) {
|
||||
InstructionRemover.remove(block, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int totalSize = 0;
|
||||
for (BlockNode excBlock : excHandler.getBlocks()) {
|
||||
totalSize += excBlock.getInstructions().size();
|
||||
}
|
||||
if (totalSize == 0 && noExitNode) {
|
||||
handlerAttr.getTryBlock().removeHandler(mth, excHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user