mirror of
https://github.com/rizinorg/cutter.git
synced 2024-12-20 11:56:12 +00:00
Use REvent for classes (#1169)
* React to Anal Class REvents * Correctly react to specific Class Events * Adapt to Class REvent changes * Update r2 submodule for Class REvents
This commit is contained in:
parent
d876c9deee
commit
bcb3a162f7
2
radare2
2
radare2
@ -1 +1 @@
|
|||||||
Subproject commit b022c9533482a8f1c9ae825ced5c1a3e251837d9
|
Subproject commit b4178702c88ed361fcb98e1b87cd74f0af4b2f44
|
@ -131,11 +131,19 @@ RCoreLocked CutterCore::core() const
|
|||||||
|
|
||||||
#define CORE_LOCK() RCoreLocked core_lock__(this->core_)
|
#define CORE_LOCK() RCoreLocked core_lock__(this->core_)
|
||||||
|
|
||||||
|
static void cutterREventCallback(REvent *, int type, void *user, void *data)
|
||||||
|
{
|
||||||
|
auto core = reinterpret_cast<CutterCore *>(user);
|
||||||
|
core->handleREvent(type, data);
|
||||||
|
}
|
||||||
|
|
||||||
CutterCore::CutterCore(QObject *parent) :
|
CutterCore::CutterCore(QObject *parent) :
|
||||||
QObject(parent)
|
QObject(parent)
|
||||||
{
|
{
|
||||||
r_cons_new(); // initialize console
|
r_cons_new(); // initialize console
|
||||||
this->core_ = r_core_new();
|
core_ = r_core_new();
|
||||||
|
|
||||||
|
r_event_hook(core_->anal->ev, R_EVENT_ALL, cutterREventCallback, this);
|
||||||
|
|
||||||
#if defined(APPIMAGE) || defined(MACOS_R2_BUNDLED)
|
#if defined(APPIMAGE) || defined(MACOS_R2_BUNDLED)
|
||||||
auto prefix = QDir(QCoreApplication::applicationDirPath());
|
auto prefix = QDir(QCoreApplication::applicationDirPath());
|
||||||
@ -1970,11 +1978,11 @@ QList<BinClassDescription> CutterCore::getAllClassesFromFlags()
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<QString> CutterCore::getAllAnalClasses()
|
QList<QString> CutterCore::getAllAnalClasses(bool sorted)
|
||||||
{
|
{
|
||||||
QList<QString> ret;
|
QList<QString> ret;
|
||||||
|
|
||||||
SdbList *l = r_anal_class_get_all(core_->anal, true);
|
SdbList *l = r_anal_class_get_all(core_->anal, sorted);
|
||||||
if (!l) {
|
if (!l) {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -2081,13 +2089,11 @@ void CutterCore::setAnalMethod(const QString &className, const AnalMethodDescrip
|
|||||||
analMeth.vtable_offset = meth.vtableOffset;
|
analMeth.vtable_offset = meth.vtableOffset;
|
||||||
r_anal_class_method_set(core_->anal, className.toUtf8().constData(), &analMeth);
|
r_anal_class_method_set(core_->anal, className.toUtf8().constData(), &analMeth);
|
||||||
r_anal_class_method_fini(&analMeth);
|
r_anal_class_method_fini(&analMeth);
|
||||||
emit classesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CutterCore::renameAnalMethod(const QString &className, const QString &oldMethodName, const QString &newMethodName)
|
void CutterCore::renameAnalMethod(const QString &className, const QString &oldMethodName, const QString &newMethodName)
|
||||||
{
|
{
|
||||||
r_anal_class_method_rename(core_->anal, className.toUtf8().constData(), oldMethodName.toUtf8().constData(), newMethodName.toUtf8().constData());
|
r_anal_class_method_rename(core_->anal, className.toUtf8().constData(), oldMethodName.toUtf8().constData(), newMethodName.toUtf8().constData());
|
||||||
emit classesChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ResourcesDescription> CutterCore::getAllResources()
|
QList<ResourcesDescription> CutterCore::getAllResources()
|
||||||
@ -2397,6 +2403,44 @@ void CutterCore::addFlag(RVA offset, QString name, RVA size)
|
|||||||
emit flagsChanged();
|
emit flagsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CutterCore::handleREvent(int type, void *data)
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case R_EVENT_CLASS_NEW: {
|
||||||
|
auto ev = reinterpret_cast<REventClass *>(data);
|
||||||
|
emit classNew(QString::fromUtf8(ev->name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case R_EVENT_CLASS_DEL: {
|
||||||
|
auto ev = reinterpret_cast<REventClass *>(data);
|
||||||
|
emit classDeleted(QString::fromUtf8(ev->name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case R_EVENT_CLASS_RENAME: {
|
||||||
|
auto ev = reinterpret_cast<REventClassRename *>(data);
|
||||||
|
emit classRenamed(QString::fromUtf8(ev->name_old), QString::fromUtf8(ev->name_new));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case R_EVENT_CLASS_ATTR_SET: {
|
||||||
|
auto ev = reinterpret_cast<REventClassAttrSet *>(data);
|
||||||
|
emit classAttrsChanged(QString::fromUtf8(ev->attr.class_name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case R_EVENT_CLASS_ATTR_DEL: {
|
||||||
|
auto ev = reinterpret_cast<REventClassAttr *>(data);
|
||||||
|
emit classAttrsChanged(QString::fromUtf8(ev->class_name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case R_EVENT_CLASS_ATTR_RENAME: {
|
||||||
|
auto ev = reinterpret_cast<REventClassAttrRename *>(data);
|
||||||
|
emit classAttrsChanged(QString::fromUtf8(ev->attr.class_name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CutterCore::triggerFlagsChanged()
|
void CutterCore::triggerFlagsChanged()
|
||||||
{
|
{
|
||||||
emit flagsChanged();
|
emit flagsChanged();
|
||||||
|
10
src/Cutter.h
10
src/Cutter.h
@ -480,7 +480,7 @@ public:
|
|||||||
void setCurrentBits(int bits, RVA offset = RVA_INVALID);
|
void setCurrentBits(int bits, RVA offset = RVA_INVALID);
|
||||||
|
|
||||||
/* Classes */
|
/* Classes */
|
||||||
QList<QString> getAllAnalClasses();
|
QList<QString> getAllAnalClasses(bool sorted);
|
||||||
QList<AnalMethodDescription> getAnalClassMethods(const QString &cls);
|
QList<AnalMethodDescription> getAnalClassMethods(const QString &cls);
|
||||||
QList<AnalBaseClassDescription> getAnalClassBaseClasses(const QString &cls);
|
QList<AnalBaseClassDescription> getAnalClassBaseClasses(const QString &cls);
|
||||||
QList<AnalVTableDescription> getAnalClassVTables(const QString &cls);
|
QList<AnalVTableDescription> getAnalClassVTables(const QString &cls);
|
||||||
@ -697,6 +697,8 @@ public:
|
|||||||
QList<StringDescription> parseStringsJson(const QJsonDocument &doc);
|
QList<StringDescription> parseStringsJson(const QJsonDocument &doc);
|
||||||
QList<FunctionDescription> parseFunctionsJson(const QJsonDocument &doc);
|
QList<FunctionDescription> parseFunctionsJson(const QJsonDocument &doc);
|
||||||
|
|
||||||
|
void handleREvent(int type, void *data);
|
||||||
|
|
||||||
/* Signals related */
|
/* Signals related */
|
||||||
void triggerVarsChanged();
|
void triggerVarsChanged();
|
||||||
void triggerFunctionRenamed(const QString &prevName, const QString &newName);
|
void triggerFunctionRenamed(const QString &prevName, const QString &newName);
|
||||||
@ -721,13 +723,17 @@ signals:
|
|||||||
void functionsChanged();
|
void functionsChanged();
|
||||||
void flagsChanged();
|
void flagsChanged();
|
||||||
void commentsChanged();
|
void commentsChanged();
|
||||||
void classesChanged();
|
|
||||||
void registersChanged();
|
void registersChanged();
|
||||||
void instructionChanged(RVA offset);
|
void instructionChanged(RVA offset);
|
||||||
void breakpointsChanged();
|
void breakpointsChanged();
|
||||||
void refreshCodeViews();
|
void refreshCodeViews();
|
||||||
void stackChanged();
|
void stackChanged();
|
||||||
|
|
||||||
|
void classNew(const QString &cls);
|
||||||
|
void classDeleted(const QString &cls);
|
||||||
|
void classRenamed(const QString &oldName, const QString &newName);
|
||||||
|
void classAttrsChanged(const QString &cls);
|
||||||
|
|
||||||
void projectSaved(bool successfully, const QString &name);
|
void projectSaved(bool successfully, const QString &name);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -14,7 +14,7 @@ EditMethodDialog::EditMethodDialog(bool classFixed, QWidget *parent) :
|
|||||||
} else {
|
} else {
|
||||||
classComboBox = new QComboBox(this);
|
classComboBox = new QComboBox(this);
|
||||||
ui->formLayout->setItem(0, QFormLayout::FieldRole, new QWidgetItem(classComboBox));
|
ui->formLayout->setItem(0, QFormLayout::FieldRole, new QWidgetItem(classComboBox));
|
||||||
for (auto &cls : Core()->getAllAnalClasses()) {
|
for (auto &cls : Core()->getAllAnalClasses(true)) {
|
||||||
classComboBox->addItem(cls, cls);
|
classComboBox->addItem(cls, cls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,19 +210,114 @@ QVariant BinClassesModel::data(const QModelIndex &index, int role) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AnalClassesModel::AnalClassesModel(QObject *parent)
|
AnalClassesModel::AnalClassesModel(CutterDockWidget *parent)
|
||||||
: ClassesModel(parent), attrs(new QMap<QString, QVector<Attribute>>)
|
: ClassesModel(parent), attrs(new QMap<QString, QVector<Attribute>>)
|
||||||
{
|
{
|
||||||
|
// Just use a simple refresh deferrer. If an event was triggered in the background, simply refresh everything later.
|
||||||
|
refreshDeferrer = parent->createRefreshDeferrer([this]() {
|
||||||
|
this->refreshAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(Core(), &CutterCore::refreshAll, this, &AnalClassesModel::refreshAll);
|
||||||
|
connect(Core(), &CutterCore::classNew, this, &AnalClassesModel::classNew);
|
||||||
|
connect(Core(), &CutterCore::classDeleted, this, &AnalClassesModel::classDeleted);
|
||||||
|
connect(Core(), &CutterCore::classRenamed, this, &AnalClassesModel::classRenamed);
|
||||||
|
connect(Core(), &CutterCore::classAttrsChanged, this, &AnalClassesModel::classAttrsChanged);
|
||||||
|
|
||||||
|
refreshAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnalClassesModel::refreshClasses()
|
void AnalClassesModel::refreshAll()
|
||||||
{
|
{
|
||||||
|
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
attrs->clear();
|
attrs->clear();
|
||||||
classes = Core()->getAllAnalClasses();
|
classes = Core()->getAllAnalClasses(true); // must be sorted
|
||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AnalClassesModel::classNew(const QString &cls)
|
||||||
|
{
|
||||||
|
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the destination position using binary search and add the row
|
||||||
|
auto it = std::lower_bound(classes.begin(), classes.end(), cls);
|
||||||
|
int index = it - classes.begin();
|
||||||
|
beginInsertRows(QModelIndex(), index, index);
|
||||||
|
classes.insert(it, cls);
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalClassesModel::classDeleted(const QString &cls)
|
||||||
|
{
|
||||||
|
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the position using binary search and remove the row
|
||||||
|
auto it = std::lower_bound(classes.begin(), classes.end(), cls);
|
||||||
|
if(it == classes.end() || *it != cls) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = it - classes.begin();
|
||||||
|
beginRemoveRows(QModelIndex(), index, index);
|
||||||
|
classes.erase(it);
|
||||||
|
endRemoveRows();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalClassesModel::classRenamed(const QString &oldName, const QString &newName)
|
||||||
|
{
|
||||||
|
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto oldIt = std::lower_bound(classes.begin(), classes.end(), oldName);
|
||||||
|
if (oldIt == classes.end() || *oldIt != oldName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto newIt = std::lower_bound(classes.begin(), classes.end(), newName);
|
||||||
|
int oldRow = oldIt - classes.begin();
|
||||||
|
int newRow = newIt - classes.begin();
|
||||||
|
// oldRow == newRow means the name stayed the same.
|
||||||
|
// oldRow == newRow - 1 means the name changed, but the row stays the same.
|
||||||
|
if (oldRow != newRow && oldRow != newRow - 1) {
|
||||||
|
beginMoveRows(QModelIndex(), oldRow, oldRow, QModelIndex(), newRow);
|
||||||
|
classes.erase(oldIt);
|
||||||
|
// iterators are invalid now, so we calculate the new position from the rows.
|
||||||
|
if (oldRow < newRow) {
|
||||||
|
// if we move down, we need to account for the removed old element above.
|
||||||
|
newRow--;
|
||||||
|
}
|
||||||
|
classes.insert(newRow, newName);
|
||||||
|
endMoveRows();
|
||||||
|
} else if (oldRow == newRow - 1) { // class name changed, but not the row
|
||||||
|
newRow--;
|
||||||
|
classes[newRow] = newName;
|
||||||
|
}
|
||||||
|
emit dataChanged(index(newRow, 0), index(newRow, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalClassesModel::classAttrsChanged(const QString &cls)
|
||||||
|
{
|
||||||
|
if (!refreshDeferrer->attemptRefresh(nullptr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = std::lower_bound(classes.begin(), classes.end(), cls);
|
||||||
|
if(it == classes.end() || *it != cls) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QPersistentModelIndex persistentIndex = QPersistentModelIndex(index(it - classes.begin(), 0));
|
||||||
|
layoutAboutToBeChanged({persistentIndex});
|
||||||
|
attrs->remove(cls);
|
||||||
|
layoutChanged({persistentIndex});
|
||||||
|
}
|
||||||
|
|
||||||
const QVector<AnalClassesModel::Attribute> &AnalClassesModel::getAttrs(const QString &cls) const
|
const QVector<AnalClassesModel::Attribute> &AnalClassesModel::getAttrs(const QString &cls) const
|
||||||
{
|
{
|
||||||
auto it = attrs->find(cls);
|
auto it = attrs->find(cls);
|
||||||
@ -439,22 +534,24 @@ bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModel
|
|||||||
case ClassesModel::OFFSET: {
|
case ClassesModel::OFFSET: {
|
||||||
RVA left_offset = left.data(ClassesModel::OffsetRole).toULongLong();
|
RVA left_offset = left.data(ClassesModel::OffsetRole).toULongLong();
|
||||||
RVA right_offset = right.data(ClassesModel::OffsetRole).toULongLong();
|
RVA right_offset = right.data(ClassesModel::OffsetRole).toULongLong();
|
||||||
if (left_offset != right_offset)
|
if (left_offset != right_offset) {
|
||||||
return left_offset < right_offset;
|
return left_offset < right_offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case ClassesModel::TYPE: {
|
case ClassesModel::TYPE: {
|
||||||
auto left_type = left.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
|
auto left_type = left.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
|
||||||
auto right_type = right.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
|
auto right_type = right.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
|
||||||
if (left_type != right_type)
|
if (left_type != right_type) {
|
||||||
return left_type < right_type;
|
return left_type < right_type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case ClassesModel::NAME:
|
case ClassesModel::NAME:
|
||||||
default:
|
default:
|
||||||
QString left_name = left.data(ClassesModel::NameRole).toString();
|
QString left_name = left.data(ClassesModel::NameRole).toString();
|
||||||
QString right_name = right.data(ClassesModel::NameRole).toString();
|
QString right_name = right.data(ClassesModel::NameRole).toString();
|
||||||
return left_name < right_name;
|
return QString::compare(left_name, right_name, Qt::CaseInsensitive) < 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,14 +577,10 @@ ClassesWidget::ClassesWidget(MainWindow *main, QAction *action) :
|
|||||||
|
|
||||||
ui->classSourceCombo->setCurrentIndex(1);
|
ui->classSourceCombo->setCurrentIndex(1);
|
||||||
|
|
||||||
connect(Core(), SIGNAL(refreshAll()), this, SLOT(refreshClasses()));
|
|
||||||
connect(Core(), &CutterCore::classesChanged, this, [this]() {
|
|
||||||
if (getSource() == Source::ANAL) {
|
|
||||||
refreshClasses();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
connect(ui->classSourceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshClasses()));
|
connect(ui->classSourceCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshClasses()));
|
||||||
connect(ui->classesTreeView, &QTreeView::customContextMenuRequested, this, &ClassesWidget::showContextMenu);
|
connect(ui->classesTreeView, &QTreeView::customContextMenuRequested, this, &ClassesWidget::showContextMenu);
|
||||||
|
|
||||||
|
refreshClasses();
|
||||||
}
|
}
|
||||||
|
|
||||||
ClassesWidget::~ClassesWidget() {}
|
ClassesWidget::~ClassesWidget() {}
|
||||||
@ -523,7 +616,6 @@ void ClassesWidget::refreshClasses()
|
|||||||
anal_model = new AnalClassesModel(this);
|
anal_model = new AnalClassesModel(this);
|
||||||
proxy_model->setSourceModel(anal_model);
|
proxy_model->setSourceModel(anal_model);
|
||||||
}
|
}
|
||||||
anal_model->refreshClasses();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +104,13 @@ private:
|
|||||||
Attribute(Type type, const QVariant &data) : type(type), data(data) {}
|
Attribute(Type type, const QVariant &data) : type(type), data(data) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* This must always stay sorted alphabetically.
|
||||||
|
*/
|
||||||
QList<QString> classes;
|
QList<QString> classes;
|
||||||
|
|
||||||
|
RefreshDeferrer *refreshDeferrer;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Cache for class attributes
|
* \brief Cache for class attributes
|
||||||
*
|
*
|
||||||
@ -130,9 +135,14 @@ private:
|
|||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AnalClassesModel(QObject *parent = nullptr);
|
explicit AnalClassesModel(CutterDockWidget *parent);
|
||||||
|
|
||||||
void refreshClasses();
|
public slots:
|
||||||
|
void refreshAll();
|
||||||
|
void classNew(const QString &cls);
|
||||||
|
void classDeleted(const QString &cls);
|
||||||
|
void classRenamed(const QString &oldName, const QString &newName);
|
||||||
|
void classAttrsChanged(const QString &cls);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,21 +17,6 @@ public:
|
|||||||
bool eventFilter(QObject *object, QEvent *event) override;
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
bool isVisibleToUser() { return isVisibleToUserCurrent; }
|
bool isVisibleToUser() { return isVisibleToUserCurrent; }
|
||||||
|
|
||||||
public slots:
|
|
||||||
void toggleDockWidget(bool show);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void becameVisibleToUser();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QAction *action;
|
|
||||||
|
|
||||||
bool isVisibleToUserCurrent = false;
|
|
||||||
void updateIsVisibleToUser();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void closeEvent(QCloseEvent *event) override;
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Convenience method for creating and registering a RefreshDeferrer without any parameters
|
* \brief Convenience method for creating and registering a RefreshDeferrer without any parameters
|
||||||
* \param refreshNowFunc lambda taking no parameters, called when a refresh should occur
|
* \param refreshNowFunc lambda taking no parameters, called when a refresh should occur
|
||||||
@ -63,6 +48,21 @@ protected:
|
|||||||
});
|
});
|
||||||
return deferrer;
|
return deferrer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void toggleDockWidget(bool show);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void becameVisibleToUser();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QAction *action;
|
||||||
|
|
||||||
|
bool isVisibleToUserCurrent = false;
|
||||||
|
void updateIsVisibleToUser();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void closeEvent(QCloseEvent *event) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // CUTTERWIDGET_H
|
#endif // CUTTERWIDGET_H
|
||||||
|
Loading…
Reference in New Issue
Block a user