Start implementation of IsoFilesystem for reading ISO images.
This commit is contained in:
parent
135ac835eb
commit
d8cb56ecf3
@ -21,6 +21,8 @@ src_libisofs_la_SOURCES = \
|
|||||||
src/fsource.h \
|
src/fsource.h \
|
||||||
src/fsource.c \
|
src/fsource.c \
|
||||||
src/fs_local.c \
|
src/fs_local.c \
|
||||||
|
src/fs_image.h \
|
||||||
|
src/fs_image.c \
|
||||||
src/messages.h \
|
src/messages.h \
|
||||||
src/messages.c \
|
src/messages.c \
|
||||||
src/libiso_msgs.h \
|
src/libiso_msgs.h \
|
||||||
|
@ -45,4 +45,8 @@
|
|||||||
|
|
||||||
#define ISO_MANGLE_TOO_MUCH_FILES -200
|
#define ISO_MANGLE_TOO_MUCH_FILES -200
|
||||||
|
|
||||||
|
/* image read errors */
|
||||||
|
#define ISO_WRONG_PVD -300
|
||||||
|
|
||||||
|
|
||||||
#endif /*LIBISO_ERROR_H_*/
|
#endif /*LIBISO_ERROR_H_*/
|
||||||
|
310
src/fs_image.c
Normal file
310
src/fs_image.c
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2007 Vreixo Formoso
|
||||||
|
*
|
||||||
|
* This file is part of the libisofs project; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License version 2 as
|
||||||
|
* published by the Free Software Foundation. See COPYING file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Filesystem/FileSource implementation to access an ISO image, using an
|
||||||
|
* IsoDataSource to read image data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "fs_image.h"
|
||||||
|
#include "error.h"
|
||||||
|
#include "ecma119.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the RR extensions be read?
|
||||||
|
*/
|
||||||
|
enum read_rr_ext {
|
||||||
|
RR_EXT_NO = 0, /*< Do not use RR extensions */
|
||||||
|
RR_EXT_110 = 1, /*< RR extensions conforming version 1.10 */
|
||||||
|
RR_EXT_112 = 2 /*< RR extensions conforming version 1.12 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private data for the image IsoFilesystem
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
/** DataSource from where data will be read */
|
||||||
|
IsoDataSource *src;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counter of the times the filesystem has been openned still pending of
|
||||||
|
* close. It is used to keep track of when we need to actually open or
|
||||||
|
* close the IsoDataSource.
|
||||||
|
*/
|
||||||
|
unsigned int open_count;
|
||||||
|
|
||||||
|
uid_t uid; /**< Default uid when no RR */
|
||||||
|
gid_t gid; /**< Default uid when no RR */
|
||||||
|
mode_t mode; /**< Default mode when no RR (only permissions) */
|
||||||
|
|
||||||
|
struct libiso_msgs *messenger;
|
||||||
|
|
||||||
|
char *input_charset; /**< Input charset for RR names */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be filled with the block lba of the extend for the root directory,
|
||||||
|
* as read from the PVM
|
||||||
|
*/
|
||||||
|
uint32_t iso_root_block;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we need to read RR extensions. i.e., if the image contains RR
|
||||||
|
* extensions, and the user wants to read them.
|
||||||
|
*/
|
||||||
|
enum read_rr_ext rr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function used to read the name from a directoy record. For ISO,
|
||||||
|
* the name is in US-ASCII. For Joliet, in UCS-2BE. Thus, we need
|
||||||
|
* different functions for both.
|
||||||
|
*/
|
||||||
|
char *(*get_name)(const char *, size_t);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joliet and RR version 1.10 does not have file serial numbers,
|
||||||
|
* we need to generate it. TODO what is this for?!?!?!
|
||||||
|
*/
|
||||||
|
//ino_t ino;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytes skipped within the System Use field of a directory record, before
|
||||||
|
* the beginning of the SUSP system user entries. See IEEE 1281, SUSP. 5.3.
|
||||||
|
*/
|
||||||
|
uint8_t len_skp;
|
||||||
|
|
||||||
|
/* Volume attributes */
|
||||||
|
char *volset_id;
|
||||||
|
char *volume_id; /**< Volume identifier. */
|
||||||
|
char *publisher_id; /**< Volume publisher. */
|
||||||
|
char *data_preparer_id; /**< Volume data preparer. */
|
||||||
|
char *system_id; /**< Volume system identifier. */
|
||||||
|
char *application_id; /**< Volume application id */
|
||||||
|
char *copyright_file_id;
|
||||||
|
char *abstract_file_id;
|
||||||
|
char *biblio_file_id;
|
||||||
|
|
||||||
|
/* extension information */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RR version being used in image.
|
||||||
|
* 0 no RR extension, 1 RRIP 1.10, 2 RRIP 1.12
|
||||||
|
*/
|
||||||
|
unsigned int rr_version : 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of blocks of the volume, as reported in the PVM.
|
||||||
|
*/
|
||||||
|
uint32_t nblocks;
|
||||||
|
|
||||||
|
//TODO el-torito information
|
||||||
|
|
||||||
|
} _ImageFsData;
|
||||||
|
|
||||||
|
static
|
||||||
|
int ifs_fs_open(IsoImageFilesystem *fs)
|
||||||
|
{
|
||||||
|
_ImageFsData *data;
|
||||||
|
|
||||||
|
if (fs == NULL || fs->fs.data == NULL) {
|
||||||
|
return ISO_NULL_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = (_ImageFsData*)fs->fs.data;
|
||||||
|
|
||||||
|
if (data->open_count == 0) {
|
||||||
|
/* we need to actually open the data source */
|
||||||
|
int res = data->src->open(data->src);
|
||||||
|
if (res < 0) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++data->open_count;
|
||||||
|
return ISO_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int ifs_fs_close(IsoImageFilesystem *fs)
|
||||||
|
{
|
||||||
|
_ImageFsData *data;
|
||||||
|
|
||||||
|
if (fs == NULL || fs->fs.data == NULL) {
|
||||||
|
return ISO_NULL_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = (_ImageFsData*)fs->fs.data;
|
||||||
|
|
||||||
|
if (--data->open_count == 0) {
|
||||||
|
/* we need to actually close the data source */
|
||||||
|
return data->src->close(data->src);
|
||||||
|
}
|
||||||
|
return ISO_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void ifs_fs_free(IsoFilesystem *fs)
|
||||||
|
{
|
||||||
|
IsoImageFilesystem *ifs;
|
||||||
|
_ImageFsData *data;
|
||||||
|
|
||||||
|
ifs = (IsoImageFilesystem*)fs;
|
||||||
|
data = (_ImageFsData*) fs->data;
|
||||||
|
|
||||||
|
/* close data source if already openned */
|
||||||
|
if (data->open_count > 0) {
|
||||||
|
data->src->close(data->src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* free our ref to datasource */
|
||||||
|
iso_data_source_unref(data->src);
|
||||||
|
|
||||||
|
/* free volume atts */
|
||||||
|
free(data->volset_id);
|
||||||
|
free(data->volume_id);
|
||||||
|
free(data->publisher_id);
|
||||||
|
free(data->data_preparer_id);
|
||||||
|
free(data->system_id);
|
||||||
|
free(data->application_id);
|
||||||
|
free(data->copyright_file_id);
|
||||||
|
free(data->abstract_file_id);
|
||||||
|
free(data->biblio_file_id);
|
||||||
|
|
||||||
|
free(data->input_charset);
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int read_pvm(_ImageFsData *data, uint32_t block)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
struct ecma119_pri_vol_desc *pvm;
|
||||||
|
struct ecma119_dir_record *rootdr;
|
||||||
|
unsigned char buffer[BLOCK_SIZE];
|
||||||
|
|
||||||
|
/* read PVM */
|
||||||
|
ret = data->src->read_block(data->src, block, buffer);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvm = (struct ecma119_pri_vol_desc *)buffer;
|
||||||
|
|
||||||
|
/* sanity checks */
|
||||||
|
if (pvm->vol_desc_type[0] != 1 || pvm->vol_desc_version[0] != 1
|
||||||
|
|| strncmp((char*)pvm->std_identifier, "CD001", 5)
|
||||||
|
|| pvm->file_structure_version[0] != 1 ) {
|
||||||
|
|
||||||
|
return ISO_WRONG_PVD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ok, it is a valid PVD */
|
||||||
|
|
||||||
|
/* fill volume attributes */
|
||||||
|
data->volset_id = strcopy((char*)pvm->vol_set_id, 128);
|
||||||
|
data->volume_id = strcopy((char*)pvm->volume_id, 32);
|
||||||
|
data->publisher_id = strcopy((char*)pvm->publisher_id, 128);
|
||||||
|
data->data_preparer_id = strcopy((char*)pvm->data_prep_id, 128);
|
||||||
|
data->system_id = strcopy((char*)pvm->system_id, 32);
|
||||||
|
data->application_id = strcopy((char*)pvm->application_id, 128);
|
||||||
|
data->copyright_file_id = strcopy((char*)pvm->copyright_file_id, 37);
|
||||||
|
data->abstract_file_id = strcopy((char*)pvm->abstract_file_id, 37);
|
||||||
|
data->biblio_file_id = strcopy((char*)pvm->bibliographic_file_id, 37);
|
||||||
|
|
||||||
|
data->nblocks = iso_read_bb(pvm->vol_space_size, 4, NULL);
|
||||||
|
|
||||||
|
rootdr = (struct ecma119_dir_record*) pvm->root_dir_record;
|
||||||
|
data->iso_root_block = iso_read_bb(rootdr->block, 4, NULL);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO
|
||||||
|
* PVM has other things that could be interesting, but that don't have a
|
||||||
|
* member in IsoImage, such as creation date. In a multisession disc, we
|
||||||
|
* could keep the creation date and update the modification date, for
|
||||||
|
* example.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return ISO_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
int iso_image_filesystem_new(IsoDataSource *src, struct iso_read_opts *opts,
|
||||||
|
IsoImageFilesystem **fs)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
IsoImageFilesystem *ifs;
|
||||||
|
_ImageFsData *data;
|
||||||
|
|
||||||
|
if (src == NULL || opts == NULL || fs == NULL) {
|
||||||
|
return ISO_NULL_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = calloc(1, sizeof(_ImageFsData));
|
||||||
|
if (data == NULL) {
|
||||||
|
return ISO_MEM_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
ifs = calloc(1, sizeof(IsoImageFilesystem));
|
||||||
|
if (ifs == NULL) {
|
||||||
|
free(data);
|
||||||
|
return ISO_MEM_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* get our ref to IsoDataSource */
|
||||||
|
data->src = src;
|
||||||
|
iso_data_source_ref(src);
|
||||||
|
data->open_count = 0; //TODO
|
||||||
|
|
||||||
|
/* fill data from opts */
|
||||||
|
data->gid = opts->gid;
|
||||||
|
data->uid = opts->uid;
|
||||||
|
data->mode = opts->mode & ~S_IFMT;
|
||||||
|
data->input_charset = strdup("UTF-8"); //TODO strdup(opts->input_charset);
|
||||||
|
data->messenger = opts->messenger;
|
||||||
|
|
||||||
|
ifs->open = ifs_fs_open;
|
||||||
|
ifs->close = ifs_fs_close;
|
||||||
|
|
||||||
|
ifs->fs.data = data;
|
||||||
|
ifs->fs.free = ifs_fs_free;
|
||||||
|
|
||||||
|
/* read Volume Descriptors and ensure it is a valid image */
|
||||||
|
|
||||||
|
/* 1. first, open the filesystem */
|
||||||
|
ifs_fs_open(ifs);
|
||||||
|
|
||||||
|
/* 2. read primary volume description */
|
||||||
|
ret = read_pvm(data, opts->block + 16);
|
||||||
|
if (ret < 0) {
|
||||||
|
goto fs_cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. read next volume descriptors */
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
/* 4. check if RR extensions are being used */
|
||||||
|
//TODO
|
||||||
|
//ret = read_root_susp_entries(info, volume->root, data->iso_root_block);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* select what tree to read */
|
||||||
|
//TODO
|
||||||
|
|
||||||
|
/* and finally return. Note that we keep the DataSource opened */
|
||||||
|
|
||||||
|
*fs = ifs;
|
||||||
|
return ISO_SUCCESS;
|
||||||
|
|
||||||
|
fs_cleanup: ;
|
||||||
|
ifs_fs_free((IsoFilesystem*)ifs);
|
||||||
|
free(ifs);
|
||||||
|
return ret;
|
||||||
|
}
|
104
src/fs_image.h
Normal file
104
src/fs_image.h
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2007 Vreixo Formoso
|
||||||
|
*
|
||||||
|
* This file is part of the libisofs project; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License version 2 as
|
||||||
|
* published by the Free Software Foundation. See COPYING file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef LIBISO_FS_IMAGE_H_
|
||||||
|
#define LIBISO_FS_IMAGE_H_
|
||||||
|
|
||||||
|
#include "libisofs.h"
|
||||||
|
#include "fsource.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for image reading.
|
||||||
|
* There are four kind of options:
|
||||||
|
* - Related to multisession support.
|
||||||
|
* In most cases, an image begins at LBA 0 of the data source. However,
|
||||||
|
* in multisession discs, the later image begins in the last session on
|
||||||
|
* disc. The block option can be used to specify the start of that last
|
||||||
|
* session.
|
||||||
|
* - Related to the tree that will be read.
|
||||||
|
* As default, when Rock Ridge extensions are present in the image, that
|
||||||
|
* will be used to get the tree. If RR extensions are not present, libisofs
|
||||||
|
* will use the Joliet extensions if available. Finally, the plain ISO-9660
|
||||||
|
* tree is used if neither RR nor Joliet extensions are available. With
|
||||||
|
* norock, nojoliet, and preferjoliet options, you can change this
|
||||||
|
* default behavior.
|
||||||
|
* - Related to default POSIX attributes.
|
||||||
|
* When Rock Ridege extensions are not used, libisofs can't figure out what
|
||||||
|
* are the the permissions, uid or gid for the files. You should supply
|
||||||
|
* default values for that.
|
||||||
|
* - Return information for image.
|
||||||
|
* Both size, hasRR and hasJoliet will be filled by libisofs with suitable values.
|
||||||
|
* Also, error is set to non-0 if some error happens (error codes are
|
||||||
|
* private now)
|
||||||
|
*/
|
||||||
|
struct iso_read_opts
|
||||||
|
{
|
||||||
|
uint32_t block; /** Block where the image begins, usually 0, can be
|
||||||
|
* different on a multisession disc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
unsigned int norock :1; /*< Do not read Rock Ridge extensions */
|
||||||
|
// unsigned int nojoliet:1; /*< Do not read Joliet extensions */
|
||||||
|
// unsigned int preferjoliet:1;
|
||||||
|
/*< When both Joliet and RR extensions are present, the RR
|
||||||
|
* tree is used. If you prefer using Joliet, set this to 1. */
|
||||||
|
|
||||||
|
uid_t uid; /**< Default uid when no RR */
|
||||||
|
gid_t gid; /**< Default uid when no RR */
|
||||||
|
mode_t mode; /**< Default mode when no RR (only permissions) */
|
||||||
|
//TODO differ file and dir mode
|
||||||
|
//option to convert names to lower case?
|
||||||
|
|
||||||
|
struct libiso_msgs *messenger;
|
||||||
|
|
||||||
|
char *input_charset;
|
||||||
|
|
||||||
|
/* modified by the function */
|
||||||
|
// unsigned int hasRR:1; /*< It will be set to 1 if RR extensions are present,
|
||||||
|
// to 0 if not. */
|
||||||
|
// unsigned int hasJoliet:1; /*< It will be set to 1 if Joliet extensions are
|
||||||
|
// present, to 0 if not. */
|
||||||
|
// uint32_t size; /**< Will be filled with the size (in 2048 byte block) of
|
||||||
|
// * the image, as reported in the PVM. */
|
||||||
|
//int error;
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct Iso_Image_Filesystem IsoImageFilesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends IsoFilesystem interface, to offer a way to access specific
|
||||||
|
* information of the image, such as several volume attributes, extensions
|
||||||
|
* being used, El-Torito artifacts...
|
||||||
|
*/
|
||||||
|
struct Iso_Image_Filesystem
|
||||||
|
{
|
||||||
|
IsoFilesystem fs;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO both open and close have meaning to other filesystems, in fact
|
||||||
|
* they seem useful for any kind of Filesystems, with the exception of
|
||||||
|
* the local filesystem. Thus, we should consider adding them to
|
||||||
|
* IsoFilesystem interface
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the filesystem for several read operations. Calling this funcion
|
||||||
|
* is not needed at all, each time that the underlying IsoDataSource need
|
||||||
|
* to be read, it is openned propertly. However, if you plan to execute
|
||||||
|
* several operations on the image, it is a good idea to open it
|
||||||
|
* previously, to prevent several open/close operations.
|
||||||
|
*/
|
||||||
|
int (*open)(IsoImageFilesystem *fs);
|
||||||
|
|
||||||
|
int (*close)(IsoImageFilesystem *fs);
|
||||||
|
};
|
||||||
|
|
||||||
|
int iso_image_filesystem_new(IsoDataSource *src, struct iso_read_opts *opts,
|
||||||
|
IsoImageFilesystem **fs);
|
||||||
|
|
||||||
|
#endif /*LIBISO_FS_IMAGE_H_*/
|
30
src/util.c
30
src/util.c
@ -483,6 +483,18 @@ uint32_t iso_read_msb(const uint8_t *buf, int bytes)
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t iso_read_bb(const uint8_t *buf, int bytes, int *error)
|
||||||
|
{
|
||||||
|
uint32_t v1 = iso_read_lsb(buf, bytes);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
uint32_t v2 = iso_read_msb(buf + bytes, bytes);
|
||||||
|
if (v1 != v2)
|
||||||
|
*error = 1;
|
||||||
|
}
|
||||||
|
return v1;
|
||||||
|
}
|
||||||
|
|
||||||
void iso_datetime_7(unsigned char *buf, time_t t)
|
void iso_datetime_7(unsigned char *buf, time_t t)
|
||||||
{
|
{
|
||||||
static int tzsetup = 0;
|
static int tzsetup = 0;
|
||||||
@ -616,3 +628,21 @@ int iso_eaccess(const char *path)
|
|||||||
}
|
}
|
||||||
return ISO_SUCCESS;
|
return ISO_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char *strcopy(const char *buf, size_t len)
|
||||||
|
{
|
||||||
|
char *str;
|
||||||
|
|
||||||
|
str = malloc((len + 1) * sizeof(char));
|
||||||
|
if (str == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
strncpy(str, buf, len);
|
||||||
|
str[len] = '\0';
|
||||||
|
|
||||||
|
/* remove trailing spaces */
|
||||||
|
for (len = len-1; str[len] == ' ' && len > 0; --len)
|
||||||
|
str[len] = '\0';
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
11
src/util.h
11
src/util.h
@ -113,6 +113,11 @@ void iso_bb(uint8_t *buf, uint32_t num, int bytes);
|
|||||||
uint32_t iso_read_lsb(const uint8_t *buf, int bytes);
|
uint32_t iso_read_lsb(const uint8_t *buf, int bytes);
|
||||||
uint32_t iso_read_msb(const uint8_t *buf, int bytes);
|
uint32_t iso_read_msb(const uint8_t *buf, int bytes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if error != NULL it will be set to 1 if LSB and MSB integers don't match.
|
||||||
|
*/
|
||||||
|
uint32_t iso_read_bb(const uint8_t *buf, int bytes, int *error);
|
||||||
|
|
||||||
/** Records the date/time into a 7 byte buffer (ECMA-119, 9.1.5) */
|
/** Records the date/time into a 7 byte buffer (ECMA-119, 9.1.5) */
|
||||||
void iso_datetime_7(uint8_t *buf, time_t t);
|
void iso_datetime_7(uint8_t *buf, time_t t);
|
||||||
|
|
||||||
@ -132,6 +137,12 @@ time_t iso_datetime_read_17(const uint8_t *buf);
|
|||||||
*/
|
*/
|
||||||
int iso_eaccess(const char *path);
|
int iso_eaccess(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy up to \p len chars from \p buf and return this newly allocated
|
||||||
|
* string. The new string is null-terminated.
|
||||||
|
*/
|
||||||
|
char *strcopy(const char *buf, size_t len);
|
||||||
|
|
||||||
typedef struct iso_rbtree IsoRBTree;
|
typedef struct iso_rbtree IsoRBTree;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user