mirror of
https://github.com/cemu-project/idapython.git
synced 2024-11-30 21:14:20 +01:00
Contributed by EiNSTeiN_:
- Fixed compilation error on Linux - Minor bugfix in hexrays.i - Added two more samples: vds3.py and vds_xrefs.py
This commit is contained in:
parent
db58b31711
commit
7eb6d04c6e
162
examples/vds3.py
Normal file
162
examples/vds3.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
""" Invert the then and else blocks of a cif_t.
|
||||||
|
|
||||||
|
Author: EiNSTeiN_ <einstein@g3nius.org>
|
||||||
|
|
||||||
|
This is a rewrite in Python of the vds3 example that comes with hexrays sdk.
|
||||||
|
|
||||||
|
|
||||||
|
The main difference with the original C code is that when we create the inverted
|
||||||
|
condition object, the newly created cexpr_t instance is given to the hexrays and
|
||||||
|
must not be freed by swig. To achieve this, we have to change the 'thisown' flag
|
||||||
|
when appropriate. See http://www.swig.org/Doc1.3/Python.html#Python_nn35
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import idautils
|
||||||
|
import idaapi
|
||||||
|
import idc
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
NETNODE_NAME = '$ hexrays-inverted-if'
|
||||||
|
|
||||||
|
class hexrays_callback_info(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.vu = None
|
||||||
|
|
||||||
|
self.node = idaapi.netnode()
|
||||||
|
if not self.node.create(NETNODE_NAME):
|
||||||
|
# node exists
|
||||||
|
self.load()
|
||||||
|
else:
|
||||||
|
self.stored = []
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
|
||||||
|
self.stored = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.node.getblob(0, 'I')
|
||||||
|
if data:
|
||||||
|
self.stored = eval(data)
|
||||||
|
print 'Invert-if: Loaded %s' % (repr(self.stored), )
|
||||||
|
except:
|
||||||
|
print 'Failed to load invert-if locations'
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.node.setblob(repr(self.stored), 0, 'I')
|
||||||
|
except:
|
||||||
|
print 'Failed to save invert-if locations'
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def invert_if(self, cfunc, insn):
|
||||||
|
|
||||||
|
if insn.opname != 'if':
|
||||||
|
return False
|
||||||
|
|
||||||
|
cif = insn.details
|
||||||
|
|
||||||
|
if not cif.ithen or not cif.ielse:
|
||||||
|
return False
|
||||||
|
|
||||||
|
idaapi.qswap(cif.ithen, cif.ielse)
|
||||||
|
cond = idaapi.cexpr_t(cif.expr)
|
||||||
|
notcond = idaapi.lnot(cond)
|
||||||
|
cond.thisown = 0 # the new wrapper 'notcond' now holds the reference to the cexpr_t
|
||||||
|
|
||||||
|
cif.expr.swap(notcond)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_location(self, ea):
|
||||||
|
if ea in self.stored:
|
||||||
|
self.stored.remove(ea)
|
||||||
|
else:
|
||||||
|
self.stored.append(ea)
|
||||||
|
self.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
def invert_if_event(self, vu):
|
||||||
|
|
||||||
|
vu.get_current_item(idaapi.USE_KEYBOARD)
|
||||||
|
item = vu.item
|
||||||
|
|
||||||
|
cfunc = vu.cfunc.__deref__()
|
||||||
|
|
||||||
|
if item.citype != idaapi.VDI_EXPR:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.invert_if(cfunc, item.it.to_specific_type):
|
||||||
|
vu.refresh_ctext()
|
||||||
|
|
||||||
|
self.add_location(item.it.ea)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def restore(self, cfunc):
|
||||||
|
|
||||||
|
#~ print 'restoring invert-if for %x' % (cfunc.entry_ea, )
|
||||||
|
|
||||||
|
str(cfunc) # generate treeitems.
|
||||||
|
|
||||||
|
restored = False
|
||||||
|
|
||||||
|
for item in cfunc.treeitems:
|
||||||
|
item = item.to_specific_type
|
||||||
|
if item.opname == 'if' and item.ea in self.stored:
|
||||||
|
if self.invert_if(cfunc, item):
|
||||||
|
restored = True
|
||||||
|
#~ print 'restore invert-if location %x' % (item.ea, )
|
||||||
|
else:
|
||||||
|
print 'invert-if location %x: NOT RESTORED' % (item.ea, )
|
||||||
|
|
||||||
|
return restored
|
||||||
|
|
||||||
|
def menu_callback(self):
|
||||||
|
try:
|
||||||
|
self.invert_if_event(self.vu)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def event_callback(self, event, *args):
|
||||||
|
|
||||||
|
try:
|
||||||
|
if event == idaapi.hxe_keyboard:
|
||||||
|
vu, keycode, shift = args
|
||||||
|
|
||||||
|
if idaapi.lookup_key_code(keycode, shift, True) == idaapi.get_key_code("I") and shift == 0:
|
||||||
|
if self.invert_if_event(vu):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
elif event == idaapi.hxe_right_click:
|
||||||
|
self.vu = args[0]
|
||||||
|
idaapi.add_custom_viewer_popup_item(self.vu.ct, "Invert then/else", "I", self.menu_callback)
|
||||||
|
|
||||||
|
elif event == idaapi.hxe_maturity:
|
||||||
|
cfunc, maturity = args
|
||||||
|
|
||||||
|
if maturity == idaapi.CMAT_FINAL:
|
||||||
|
self.restore(cfunc)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if idaapi.init_hexrays_plugin():
|
||||||
|
i = hexrays_callback_info()
|
||||||
|
idaapi.install_hexrays_callback(i.event_callback)
|
||||||
|
else:
|
||||||
|
print 'invert-if: hexrays is not available.'
|
319
examples/vds_xrefs.py
Normal file
319
examples/vds_xrefs.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
""" Xref plugin for Hexrays Decompiler
|
||||||
|
|
||||||
|
Author: EiNSTeiN_ <einstein@g3nius.org>
|
||||||
|
|
||||||
|
Show decompiler-style Xref when the X key is pressed in the Decompiler window.
|
||||||
|
|
||||||
|
- It supports any global name: functions, strings, integers, etc.
|
||||||
|
- It supports structure member.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import idautils
|
||||||
|
import idaapi
|
||||||
|
import idc
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
print 'Using PyQt'
|
||||||
|
except:
|
||||||
|
print 'PyQt not available'
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide import QtGui, QtCore
|
||||||
|
print 'Using PySide'
|
||||||
|
except:
|
||||||
|
print 'PySide not available'
|
||||||
|
|
||||||
|
XREF_EA = 0
|
||||||
|
XREF_STRUC_MEMBER = 1
|
||||||
|
|
||||||
|
class XrefsForm(idaapi.PluginForm):
|
||||||
|
|
||||||
|
def __init__(self, target):
|
||||||
|
|
||||||
|
idaapi.PluginForm.__init__(self)
|
||||||
|
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
if type(self.target) == idaapi.cfunc_t:
|
||||||
|
|
||||||
|
self.__type = XREF_EA
|
||||||
|
self.__ea = self.target.entry_ea
|
||||||
|
self.__name = 'Xrefs of %x' % (self.__ea, )
|
||||||
|
|
||||||
|
elif type(self.target) == idaapi.cexpr_t and self.target.opname == 'obj':
|
||||||
|
|
||||||
|
self.__type = XREF_EA
|
||||||
|
self.__ea = self.target.obj_ea
|
||||||
|
self.__name = 'Xrefs of %x' % (self.__ea, )
|
||||||
|
|
||||||
|
elif type(self.target) == idaapi.cexpr_t and self.target.opname in ('memptr', 'memref'):
|
||||||
|
|
||||||
|
self.__type = XREF_STRUC_MEMBER
|
||||||
|
name = self.get_struc_name()
|
||||||
|
self.__name = 'Xrefs of %s' % (name, )
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError('cannot show xrefs for this kind of target')
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_struc_name(self):
|
||||||
|
|
||||||
|
x = self.target.operands['x']
|
||||||
|
m = self.target.operands['m']
|
||||||
|
|
||||||
|
xtype = typestring(x.type.u_str())
|
||||||
|
xtype.remove_ptr_or_array()
|
||||||
|
typename = str(xtype)
|
||||||
|
|
||||||
|
sid = idc.GetStrucIdByName(typename)
|
||||||
|
member = idc.GetMemberName(sid, m)
|
||||||
|
|
||||||
|
return '%s::%s' % (typename, member)
|
||||||
|
|
||||||
|
def OnCreate(self, form):
|
||||||
|
|
||||||
|
# Get parent widget
|
||||||
|
try:
|
||||||
|
self.parent = self.FormToPySideWidget(form)
|
||||||
|
except:
|
||||||
|
self.parent = self.FormToPyQtWidget(form)
|
||||||
|
|
||||||
|
self.populate_form()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def Show(self):
|
||||||
|
idaapi.PluginForm.Show(self, self.__name)
|
||||||
|
return
|
||||||
|
|
||||||
|
def populate_form(self):
|
||||||
|
# Create layout
|
||||||
|
layout = QtGui.QVBoxLayout()
|
||||||
|
|
||||||
|
layout.addWidget(QtGui.QLabel(self.__name))
|
||||||
|
self.table = QtGui.QTableWidget()
|
||||||
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
|
self.table.setColumnCount(3)
|
||||||
|
self.table.setHorizontalHeaderItem(0, QtGui.QTableWidgetItem("Address"))
|
||||||
|
self.table.setHorizontalHeaderItem(1, QtGui.QTableWidgetItem("Function"))
|
||||||
|
self.table.setHorizontalHeaderItem(2, QtGui.QTableWidgetItem("Line"))
|
||||||
|
|
||||||
|
self.table.setColumnWidth(0, 80)
|
||||||
|
self.table.setColumnWidth(1, 150)
|
||||||
|
self.table.setColumnWidth(2, 450)
|
||||||
|
|
||||||
|
self.table.cellDoubleClicked.connect(self.double_clicked)
|
||||||
|
|
||||||
|
#~ self.table.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
|
||||||
|
self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows )
|
||||||
|
self.parent.setLayout(layout)
|
||||||
|
|
||||||
|
self.populate_table()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def double_clicked(self, row, column):
|
||||||
|
|
||||||
|
ea = self.functions[row]
|
||||||
|
idaapi.open_pseudocode(ea, True)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_decompiled_line(self, cfunc, ea):
|
||||||
|
|
||||||
|
print repr(ea)
|
||||||
|
if ea not in cfunc.eamap:
|
||||||
|
print 'strange, %x is not in %x eamap' % (ea, cfunc.entry_ea)
|
||||||
|
return
|
||||||
|
|
||||||
|
insnvec = cfunc.eamap[ea]
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for stmt in insnvec:
|
||||||
|
|
||||||
|
qs = idaapi.qstring()
|
||||||
|
qp = idaapi.qstring_printer_t(cfunc.__deref__(), qs, False)
|
||||||
|
|
||||||
|
stmt._print(0, qp)
|
||||||
|
s = str(qs).split('\n')[0]
|
||||||
|
|
||||||
|
#~ s = idaapi.tag_remove(s)
|
||||||
|
lines.append(s)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def get_items_for_ea(self, ea):
|
||||||
|
|
||||||
|
frm = [x.frm for x in idautils.XrefsTo(self.__ea)]
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for ea in frm:
|
||||||
|
try:
|
||||||
|
cfunc = idaapi.decompile(ea)
|
||||||
|
cfunc.refcnt += 1
|
||||||
|
|
||||||
|
self.functions.append(cfunc.entry_ea)
|
||||||
|
self.items.append((ea, idc.GetFunctionName(cfunc.entry_ea), self.get_decompiled_line(cfunc, ea)))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print 'could not decompile: %s' % (str(e), )
|
||||||
|
raise
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_items_for_type(self):
|
||||||
|
|
||||||
|
x = self.target.operands['x']
|
||||||
|
m = self.target.operands['m']
|
||||||
|
|
||||||
|
xtype = typestring(x.type.u_str())
|
||||||
|
xtype.remove_ptr_or_array()
|
||||||
|
typename = str(xtype)
|
||||||
|
|
||||||
|
addresses = []
|
||||||
|
for ea in idautils.Functions():
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfunc = idaapi.decompile(ea)
|
||||||
|
cfunc.refcnt += 1
|
||||||
|
except:
|
||||||
|
print 'Decompilation of %x failed' % (ea, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
str(cfunc)
|
||||||
|
|
||||||
|
for citem in cfunc.treeitems:
|
||||||
|
citem = citem.to_specific_type
|
||||||
|
if not (type(citem) == idaapi.cexpr_t and citem.opname in ('memptr', 'memref')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
_x = citem.operands['x']
|
||||||
|
_m = citem.operands['m']
|
||||||
|
_xtype = typestring(_x.type.u_str())
|
||||||
|
_xtype.remove_ptr_or_array()
|
||||||
|
_typename = str(_xtype)
|
||||||
|
|
||||||
|
#~ print 'in', hex(cfunc.entry_ea), _typename, _m
|
||||||
|
|
||||||
|
if not (_typename == typename and _m == m):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parent = citem
|
||||||
|
while parent:
|
||||||
|
if type(parent.to_specific_type) == idaapi.cinsn_t:
|
||||||
|
break
|
||||||
|
parent = cfunc.body.find_parent_of(parent)
|
||||||
|
|
||||||
|
if not parent:
|
||||||
|
print 'cannot find parent statement (?!)'
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parent.ea in addresses:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parent.ea == idaapi.BADADDR:
|
||||||
|
print 'parent.ea is BADADDR'
|
||||||
|
continue
|
||||||
|
|
||||||
|
addresses.append(parent.ea)
|
||||||
|
|
||||||
|
self.functions.append(cfunc.entry_ea)
|
||||||
|
self.items.append((parent.ea, idc.GetFunctionName(cfunc.entry_ea), self.get_decompiled_line(cfunc, int(parent.ea))))
|
||||||
|
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def populate_table(self):
|
||||||
|
|
||||||
|
self.functions = []
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
if self.__type == XREF_EA:
|
||||||
|
self.get_items_for_ea(self.__ea)
|
||||||
|
else:
|
||||||
|
self.get_items_for_type()
|
||||||
|
|
||||||
|
self.table.setRowCount(len(self.items))
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for item in self.items:
|
||||||
|
address, func, line = item
|
||||||
|
item = QtGui.QTableWidgetItem('0x%x' % (address, ))
|
||||||
|
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(i, 0, item)
|
||||||
|
item = QtGui.QTableWidgetItem(func)
|
||||||
|
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(i, 1, item)
|
||||||
|
item = QtGui.QTableWidgetItem(line)
|
||||||
|
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable)
|
||||||
|
self.table.setItem(i, 2, item)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
self.table.resizeRowsToContents()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def OnClose(self, form):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class hexrays_callback_info(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.vu = None
|
||||||
|
return
|
||||||
|
|
||||||
|
def show_xrefs(self, vu):
|
||||||
|
|
||||||
|
vu.get_current_item(idaapi.USE_KEYBOARD)
|
||||||
|
item = vu.item
|
||||||
|
|
||||||
|
sel = None
|
||||||
|
if item.citype == idaapi.VDI_EXPR and item.it.to_specific_type.opname in ('obj', 'memref', 'memptr'):
|
||||||
|
# if an expression is selected. verify that it's either a cot_obj, cot_memref or cot_memptr
|
||||||
|
sel = item.it.to_specific_type
|
||||||
|
|
||||||
|
elif item.citype == idaapi.VDI_FUNC:
|
||||||
|
# if the function itself is selected, show xrefs to it.
|
||||||
|
sel = item.f
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
form = XrefsForm(sel)
|
||||||
|
form.Show()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def menu_callback(self):
|
||||||
|
self.show_xrefs(self.vu)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def event_callback(self, event, *args):
|
||||||
|
|
||||||
|
try:
|
||||||
|
if event == idaapi.hxe_keyboard:
|
||||||
|
vu, keycode, shift = args
|
||||||
|
|
||||||
|
if idaapi.lookup_key_code(keycode, shift, True) == idaapi.get_key_code("X") and shift == 0:
|
||||||
|
if self.show_xrefs(vu):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
elif event == idaapi.hxe_right_click:
|
||||||
|
self.vu = args[0]
|
||||||
|
idaapi.add_custom_viewer_popup_item(self.vu.ct, "Xrefs", "X", self.menu_callback)
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if idaapi.init_hexrays_plugin():
|
||||||
|
i = hexrays_callback_info()
|
||||||
|
idaapi.install_hexrays_callback(i.event_callback)
|
||||||
|
else:
|
||||||
|
print 'invert-if: hexrays is not available.'
|
@ -754,15 +754,15 @@ citem_t.to_specific_type = property(citem_to_specific_type)
|
|||||||
|
|
||||||
""" array used for translating cinsn_t->op type to their names. """
|
""" array used for translating cinsn_t->op type to their names. """
|
||||||
cinsn_t.op_to_typename = {}
|
cinsn_t.op_to_typename = {}
|
||||||
for k in dir(globals()):
|
for k in dir(_idaapi):
|
||||||
if k.startswith('cit_'):
|
if k.startswith('cit_'):
|
||||||
cinsn_t.op_to_typename[getattr(globals(), k)] = k[4:]
|
cinsn_t.op_to_typename[getattr(_idaapi, k)] = k[4:]
|
||||||
|
|
||||||
""" array used for translating cexpr_t->op type to their names. """
|
""" array used for translating cexpr_t->op type to their names. """
|
||||||
cexpr_t.op_to_typename = {}
|
cexpr_t.op_to_typename = {}
|
||||||
for k in dir(globals()):
|
for k in dir(_idaapi):
|
||||||
if k.startswith('cot_'):
|
if k.startswith('cot_'):
|
||||||
cexpr_t.op_to_typename[getattr(globals(), k)] = k[4:]
|
cexpr_t.op_to_typename[getattr(_idaapi, k)] = k[4:]
|
||||||
|
|
||||||
def property_op_to_typename(self):
|
def property_op_to_typename(self):
|
||||||
return self.op_to_typename[self.op]
|
return self.op_to_typename[self.op]
|
||||||
@ -1066,7 +1066,7 @@ def _map_as_dict(maptype, name, keytype, valuetype):
|
|||||||
|
|
||||||
for fctname in ['begin', 'end', 'first', 'second', 'next', \
|
for fctname in ['begin', 'end', 'first', 'second', 'next', \
|
||||||
'find', 'insert', 'erase', 'clear', 'size']:
|
'find', 'insert', 'erase', 'clear', 'size']:
|
||||||
fct = globals()[name + '_' + fctname]
|
fct = getattr(_idaapi, name + '_' + fctname)
|
||||||
setattr(maptype, '__' + fctname, fct)
|
setattr(maptype, '__' + fctname, fct)
|
||||||
|
|
||||||
maptype.__len__ = maptype.size
|
maptype.__len__ = maptype.size
|
||||||
|
@ -82,7 +82,7 @@ $result = PyLong_FromUnsignedLongLong((unsigned long long) $1);
|
|||||||
%ignore setflag;
|
%ignore setflag;
|
||||||
%ignore read2bytes;
|
%ignore read2bytes;
|
||||||
%ignore rotate_left;
|
%ignore rotate_left;
|
||||||
%ignore qswap;
|
//%ignore qswap;
|
||||||
%ignore swap32;
|
%ignore swap32;
|
||||||
%ignore swap16;
|
%ignore swap16;
|
||||||
%ignore swap_value;
|
%ignore swap_value;
|
||||||
@ -108,6 +108,10 @@ $result = PyLong_FromUnsignedLongLong((unsigned long long) $1);
|
|||||||
%ignore qstrtok;
|
%ignore qstrtok;
|
||||||
%ignore qstrlwr;
|
%ignore qstrlwr;
|
||||||
%ignore qstrupr;
|
%ignore qstrupr;
|
||||||
|
|
||||||
|
void qvector<uval_t>::grow(const unsigned int &x=0);
|
||||||
|
%ignore qvector<uval_t>::grow;
|
||||||
|
|
||||||
%include "pro.h"
|
%include "pro.h"
|
||||||
|
|
||||||
//---------------------------------------------------------------------
|
//---------------------------------------------------------------------
|
||||||
|
Loading…
Reference in New Issue
Block a user