mirror of
https://github.com/RPCS3/rpcs3.git
synced 2025-01-30 12:32:43 +00:00
Further implemented sys_fs_get_mount_info() and sys_fs_get_mount_info_size()
This commit is contained in:
parent
16098c38c8
commit
13ca1a7f09
@ -20,17 +20,32 @@
|
||||
|
||||
LOG_CHANNEL(sys_fs);
|
||||
|
||||
lv2_fs_mount_point g_mp_sys_dev_root;
|
||||
lv2_fs_mount_point g_mp_sys_no_device;
|
||||
lv2_fs_mount_point g_mp_sys_dev_hdd0{"/dev_hdd0", 512, 0x24FAEA98};
|
||||
lv2_fs_mount_point g_mp_sys_dev_hdd1{"/dev_hdd1", 512, 0x3FFFF8, 32768, lv2_mp_flag::no_uid_gid + lv2_mp_flag::cache};
|
||||
lv2_fs_mount_point g_mp_sys_dev_usb{"", 512, 0x100, 4096, lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_bdvd{"", 2048, 0x4D955, 65536, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_dvd{"", 2048, 0x100, 32768, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_host_root{"", 512, 0x100, 512, lv2_mp_flag::strict_get_block_size + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash{"", 512, 0x63E00, 8192, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash2{"", 512, 0x8000, 8192, lv2_mp_flag::no_uid_gid}; // TODO confirm
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash3{"", 512, 0x400, 8192, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid}; // TODO confirm
|
||||
lv2_fs_mount_point g_mp_sys_dev_root{"/", "CELL_FS_ADMINFS", "CELL_FS_ADMINFS:"};
|
||||
lv2_fs_mount_point g_mp_sys_app_home{"/app_home", "CELL_FS_DUMMYFS", "CELL_FS_DUMMY:", 512, 0x100, 512, lv2_mp_flag::strict_get_block_size + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_host_root{"/host_root", "CELL_FS_DUMMYFS", "CELL_FS_DUMMY:/", 512, 0x100, 512, lv2_mp_flag::strict_get_block_size + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash{"/dev_flash", "CELL_FS_FAT", "CELL_FS_IOS:BUILTIN_FLSH1", 512, 0x63E00, 8192, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash2{"/dev_flash2", "CELL_FS_FAT", "CELL_FS_IOS:BUILTIN_FLSH2", 512, 0x8000, 8192, lv2_mp_flag::no_uid_gid}; // TODO confirm
|
||||
lv2_fs_mount_point g_mp_sys_dev_flash3{"/dev_flash3", "CELL_FS_FAT", "CELL_FS_IOS:BUILTIN_FLSH3", 512, 0x400, 8192, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid}; // TODO confirm
|
||||
lv2_fs_mount_point g_mp_sys_dev_hdd0{"/dev_hdd0", "CELL_FS_UFS", "CELL_FS_UTILITY:HDD0", 512, 0x24FAEA98};
|
||||
lv2_fs_mount_point g_mp_sys_dev_hdd1{"/dev_hdd1", "CELL_FS_FAT", "CELL_FS_UTILITY:HDD1", 512, 0x3FFFF8, 32768, lv2_mp_flag::no_uid_gid + lv2_mp_flag::cache};
|
||||
lv2_fs_mount_point g_mp_sys_dev_bdvd{"/dev_bdvd", "CELL_FS_ISO9660", "CELL_FS_IOS:PATA0_BDVD_DRIVE", 2048, 0x4D955, 65536, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_dvd{"/dev_ps2disc", "CELL_FS_ISO9660", "CELL_FS_IOS:PATA0_BDVD_DRIVE", 2048, 0x100, 32768, lv2_mp_flag::read_only + lv2_mp_flag::no_uid_gid};
|
||||
lv2_fs_mount_point g_mp_sys_dev_usb{"/dev_usb", "CELL_FS_FAT", "CELL_FS_IOS:USB_MASS_STORAGE", 512, 0x100, 4096, lv2_mp_flag::no_uid_gid};
|
||||
constexpr lv2_fs_mount_point* mp_array[]
|
||||
{
|
||||
&g_mp_sys_dev_root,
|
||||
&g_mp_sys_app_home,
|
||||
&g_mp_sys_host_root,
|
||||
&g_mp_sys_dev_flash,
|
||||
&g_mp_sys_dev_flash2,
|
||||
&g_mp_sys_dev_flash3,
|
||||
&g_mp_sys_dev_hdd0,
|
||||
&g_mp_sys_dev_hdd1,
|
||||
&g_mp_sys_dev_bdvd,
|
||||
&g_mp_sys_dev_dvd,
|
||||
&g_mp_sys_dev_usb,
|
||||
};
|
||||
|
||||
template<>
|
||||
void fmt_class_string<lv2_file_type>::format(std::string& out, u64 arg)
|
||||
@ -178,72 +193,65 @@ lv2_fs_mount_point* lv2_fs_object::get_mp(std::string_view filename)
|
||||
{
|
||||
const auto mp_name = get_device_path(filename);
|
||||
|
||||
if (!mp_name.empty())
|
||||
{
|
||||
if (mp_name == "dev_hdd0"sv)
|
||||
return &g_mp_sys_dev_hdd0;
|
||||
if (mp_name == "dev_hdd1"sv)
|
||||
return &g_mp_sys_dev_hdd1;
|
||||
if (mp_name.starts_with("dev_usb"sv))
|
||||
return &g_mp_sys_dev_usb;
|
||||
if (mp_name == "dev_bdvd"sv)
|
||||
return &g_mp_sys_dev_bdvd;
|
||||
if (mp_name == "dev_ps2disc"sv)
|
||||
return &g_mp_sys_dev_dvd;
|
||||
if (mp_name == "app_home"sv && filename.data() != Emu.argv[0].data())
|
||||
return lv2_fs_object::get_mp(Emu.argv[0]);
|
||||
if (mp_name == "host_root"sv)
|
||||
return &g_mp_sys_host_root;
|
||||
if (mp_name == "dev_flash"sv)
|
||||
return &g_mp_sys_dev_flash;
|
||||
if (mp_name == "dev_flash2"sv)
|
||||
return &g_mp_sys_dev_flash2;
|
||||
if (mp_name == "dev_flash3"sv)
|
||||
return &g_mp_sys_dev_flash3;
|
||||
|
||||
// Default
|
||||
if (mp_name == "dev_hdd0"sv)
|
||||
return &g_mp_sys_dev_hdd0;
|
||||
}
|
||||
if (mp_name == "dev_hdd1"sv)
|
||||
return &g_mp_sys_dev_hdd1;
|
||||
if (mp_name.starts_with("dev_usb"sv))
|
||||
return &g_mp_sys_dev_usb;
|
||||
if (mp_name == "dev_bdvd"sv)
|
||||
return &g_mp_sys_dev_bdvd;
|
||||
if (mp_name == "dev_ps2disc"sv)
|
||||
return &g_mp_sys_dev_dvd;
|
||||
if (mp_name == "app_home"sv)
|
||||
return &g_mp_sys_app_home;
|
||||
if (mp_name == "host_root"sv)
|
||||
return &g_mp_sys_host_root;
|
||||
if (mp_name == "dev_flash"sv)
|
||||
return &g_mp_sys_dev_flash;
|
||||
if (mp_name == "dev_flash2"sv)
|
||||
return &g_mp_sys_dev_flash2;
|
||||
if (mp_name == "dev_flash3"sv)
|
||||
return &g_mp_sys_dev_flash3;
|
||||
|
||||
// Default fallback
|
||||
return &g_mp_sys_dev_root;
|
||||
return &g_mp_sys_no_device;
|
||||
}
|
||||
|
||||
std::string lv2_fs_object::get_vfs(std::string_view filename)
|
||||
{
|
||||
const auto mp_name = get_device_path(filename);
|
||||
|
||||
if (!mp_name.empty())
|
||||
{
|
||||
if (mp_name == "dev_hdd0"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_hdd0, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name == "dev_hdd1"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_hdd1, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name.starts_with("dev_usb"sv))
|
||||
return g_cfg_vfs.get_device(g_cfg_vfs.dev_usb, fmt::format("/%s", mp_name), rpcs3::utils::get_emu_dir()).path;
|
||||
if (mp_name == "dev_bdvd"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_bdvd, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name == "dev_ps2disc"sv)
|
||||
return {}; // Unsupported in VFS
|
||||
if (mp_name == "app_home"sv && filename.data() != Emu.argv[0].data())
|
||||
return lv2_fs_object::get_vfs(Emu.argv[0]);
|
||||
if (mp_name == "host_root"sv)
|
||||
return g_cfg.vfs.host_root ? "/" : std::string();
|
||||
if (mp_name == "dev_flash"sv)
|
||||
return g_cfg_vfs.get_dev_flash();
|
||||
if (mp_name == "dev_flash2"sv)
|
||||
return g_cfg_vfs.get_dev_flash2();
|
||||
if (mp_name == "dev_flash3"sv)
|
||||
return g_cfg_vfs.get_dev_flash3();
|
||||
|
||||
// Default
|
||||
if (mp_name == "dev_hdd0"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_hdd0, rpcs3::utils::get_emu_dir());
|
||||
}
|
||||
if (mp_name == "dev_hdd1"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_hdd1, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name.starts_with("dev_usb"sv))
|
||||
return g_cfg_vfs.get_device(g_cfg_vfs.dev_usb, fmt::format("/%s", mp_name), rpcs3::utils::get_emu_dir()).path;
|
||||
if (mp_name == "dev_bdvd"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.dev_bdvd, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name == "dev_ps2disc"sv)
|
||||
return {}; // Unsupported in VFS
|
||||
if (mp_name == "app_home"sv)
|
||||
return g_cfg_vfs.get(g_cfg_vfs.app_home, rpcs3::utils::get_emu_dir());
|
||||
if (mp_name == "host_root"sv)
|
||||
return g_cfg.vfs.host_root ? "/" : std::string();
|
||||
if (mp_name == "dev_flash"sv)
|
||||
return g_cfg_vfs.get_dev_flash();
|
||||
if (mp_name == "dev_flash2"sv)
|
||||
return g_cfg_vfs.get_dev_flash2();
|
||||
if (mp_name == "dev_flash3"sv)
|
||||
return g_cfg_vfs.get_dev_flash3();
|
||||
|
||||
// Default fallback
|
||||
return {};
|
||||
}
|
||||
|
||||
s32 lv2_fs_object::get_mount_count()
|
||||
{
|
||||
return std::count_if(std::begin(mp_array), std::end(mp_array), [](lv2_fs_mount_point* mp){return mp != &g_mp_sys_dev_usb && mp->is_mounted == 1U;}) + std::popcount(g_mp_sys_dev_usb.is_mounted);
|
||||
}
|
||||
|
||||
lv2_fs_object::lv2_fs_object(utils::serial& ar, bool)
|
||||
: name(ar)
|
||||
, mp(get_mp(name.data()))
|
||||
@ -2932,54 +2940,62 @@ error_code sys_fs_truncate2(ppu_thread&, u32 fd, u64 size)
|
||||
|
||||
error_code sys_fs_get_mount_info_size(ppu_thread&, vm::ptr<u64> len)
|
||||
{
|
||||
sys_fs.todo("sys_fs_get_mount_info_size(len=*0x%x)", len);
|
||||
sys_fs.trace("sys_fs_get_mount_info_size(len=*0x%x)", len);
|
||||
|
||||
if (!len)
|
||||
{
|
||||
return CELL_EFAULT;
|
||||
}
|
||||
|
||||
*len = 0x9;
|
||||
*len = lv2_fs_object::get_mount_count();
|
||||
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
error_code sys_fs_get_mount_info(ppu_thread&, vm::ptr<CellFsMountInfo> info, u32 len, vm::ptr<u64> out_len)
|
||||
{
|
||||
sys_fs.todo("sys_fs_get_mount_info(info=*0x%x, len=0x%x, out_len=*0x%x)", info, len, out_len);
|
||||
sys_fs.trace("sys_fs_get_mount_info(info=*0x%x, len=0x%x, out_len=*0x%x)", info, len, out_len);
|
||||
|
||||
if (!out_len)
|
||||
if (!info || !out_len)
|
||||
{
|
||||
return CELL_EFAULT;
|
||||
}
|
||||
|
||||
// TODO there is a case where 'something' happens if !info or len == 0
|
||||
if (!info || len == 0)
|
||||
{
|
||||
sys_fs.todo("sys_fs_get_mount_info special case TODO");
|
||||
}
|
||||
|
||||
const u32 max_len = std::min<u32>(len, 9);
|
||||
const u32 max_len = std::min<u32>(len, lv2_fs_object::get_mount_count());
|
||||
*out_len = max_len;
|
||||
|
||||
struct mount_info
|
||||
{
|
||||
std::string_view path, filesystem, dev_name;
|
||||
std::string path, filesystem, dev_name;
|
||||
be_t<u32> unk1 = 0, unk2 = 0, unk3 = 0, unk4 = 0, unk5 = 0;
|
||||
};
|
||||
|
||||
static constexpr std::array<mount_info, 9> data
|
||||
std::vector<mount_info> data;
|
||||
|
||||
for (auto mp : mp_array)
|
||||
{
|
||||
mount_info{.path = "/", .filesystem = "CELL_FS_ADMINFS", .dev_name = "CELL_FS_ADMINFS:", .unk5 = 0x10000000},
|
||||
mount_info{.path = "/app_home", .filesystem = "CELL_FS_DUMMY", .dev_name = "CELL_FS_DUMMY:"},
|
||||
mount_info{.path = "/host_root", .filesystem = "CELL_FS_DUMMY", .dev_name = "CELL_FS_DUMMY:"},
|
||||
mount_info{.path = "/dev_flash", .filesystem = "CELL_FS_FAT", .dev_name = "CELL_FS_IOS:BUILTIN_FLSH1:", .unk5 = 0x10000000},
|
||||
mount_info{.path = "/dev_flash2", .filesystem = "CELL_FS_FAT", .dev_name = "CELL_FS_IOS:BUILTIN_FLSH2:"},
|
||||
mount_info{.path = "/dev_flash3", .filesystem = "CELL_FS_FAT", .dev_name = "CELL_FS_IOS:BUILTIN_FLSH3:"},
|
||||
mount_info{.path = "/dev_hdd0", .filesystem = "CELL_FS_UFS", .dev_name = "CELL_FS_UTILITY:HDD0:"},
|
||||
mount_info{.path = "/dev_hdd1", .filesystem = "CELL_FS_FAT", .dev_name = "CELL_FS_UTILITY:HDD1:"},
|
||||
mount_info{.path = "/dev_bdvd", .filesystem = "CELL_FS_ISO9660", .dev_name = "CELL_FS_IOS:PATA0_BDVD_DRIVE"},
|
||||
};
|
||||
if (mp->is_mounted == 0U)
|
||||
continue;
|
||||
|
||||
if (mp == &g_mp_sys_dev_usb)
|
||||
{
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if ((mp->is_mounted >> i) & 1U)
|
||||
{
|
||||
data.push_back(mount_info{.path = fmt::format("%s%03d", mp->root, i), .filesystem = mp->file_system.data(), .dev_name = fmt::format("%s%03d", mp->device, i)});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (mp == &g_mp_sys_dev_root || mp == &g_mp_sys_dev_flash)
|
||||
{
|
||||
data.push_back(mount_info{.path = mp->root.data(), .filesystem = mp->file_system.data(), .dev_name = mp->device.data(), .unk5 = 0x10000000});
|
||||
}
|
||||
else
|
||||
{
|
||||
data.push_back(mount_info{.path = mp->root.data(), .filesystem = mp->file_system.data(), .dev_name = mp->device.data()});
|
||||
}
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < max_len; info++, i++)
|
||||
{
|
||||
@ -2992,7 +3008,7 @@ error_code sys_fs_get_mount_info(ppu_thread&, vm::ptr<CellFsMountInfo> info, u32
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
error_code sys_fs_newfs(ppu_thread& ppu, vm::cptr<char> dev_name, vm::cptr<char> file_system, s32 unk1, vm::cptr<char> str1)
|
||||
error_code sys_fs_newfs(ppu_thread&, vm::cptr<char> dev_name, vm::cptr<char> file_system, s32 unk1, vm::cptr<char> str1)
|
||||
{
|
||||
sys_fs.todo("sys_fs_newfs(dev_name=%s, file_system=%s, unk1=0x%x, str1=%s)", dev_name, file_system, unk1, str1);
|
||||
|
||||
@ -3032,6 +3048,12 @@ error_code sys_fs_mount(ppu_thread&, vm::cptr<char> dev_name, vm::cptr<char> fil
|
||||
|
||||
auto vfs_mount = [&vpath = vpath, &filesystem = filesystem, &mp = mp](std::string mount_path)
|
||||
{
|
||||
const std::string local_path = vfs::get(vpath);
|
||||
if (!local_path.empty())
|
||||
{
|
||||
sys_fs.error("\"%s\" has already been mounted to \"%s\"", vpath, local_path);
|
||||
return false;
|
||||
}
|
||||
if (!mount_path.ends_with('/'))
|
||||
mount_path += '/';
|
||||
if (!fs::is_dir(mount_path) && !fs::create_dir(mount_path))
|
||||
@ -3097,6 +3119,11 @@ error_code sys_fs_unmount(ppu_thread&, vm::cptr<char> path, s32 unk1, s32 unk2)
|
||||
auto vfs_unmount = [&vpath = vpath]()
|
||||
{
|
||||
const std::string local_path = vfs::get(vpath);
|
||||
if (local_path.empty())
|
||||
{
|
||||
sys_fs.error("\"%s\" is not mounted!", vpath);
|
||||
return false;
|
||||
}
|
||||
if (fs::is_file(local_path))
|
||||
{
|
||||
if (fs::remove_file(local_path))
|
||||
|
@ -148,16 +148,20 @@ enum class lv2_file_type
|
||||
struct lv2_fs_mount_point
|
||||
{
|
||||
const std::string_view root;
|
||||
const std::string_view file_system;
|
||||
const std::string_view device;
|
||||
const u32 sector_size = 512;
|
||||
const u64 sector_count = 256;
|
||||
const u32 block_size = 4096;
|
||||
const bs_t<lv2_mp_flag> flags{};
|
||||
u8 is_mounted = 0U;
|
||||
|
||||
mutable std::recursive_mutex mutex;
|
||||
};
|
||||
|
||||
extern lv2_fs_mount_point g_mp_sys_dev_hdd0;
|
||||
extern lv2_fs_mount_point g_mp_sys_dev_hdd1;
|
||||
extern lv2_fs_mount_point g_mp_sys_dev_usb;
|
||||
|
||||
struct lv2_fs_object
|
||||
{
|
||||
@ -190,6 +194,7 @@ public:
|
||||
static std::string_view get_device_path(std::string_view filename);
|
||||
static lv2_fs_mount_point* get_mp(std::string_view filename);
|
||||
static std::string get_vfs(std::string_view filename);
|
||||
static s32 get_mount_count();
|
||||
|
||||
static std::array<char, 0x420> get_name(std::string_view filename)
|
||||
{
|
||||
@ -617,8 +622,8 @@ error_code sys_fs_lsn_write(ppu_thread& ppu, u32 fd, vm::cptr<void>, u64);
|
||||
error_code sys_fs_mapped_allocate(ppu_thread& ppu, u32 fd, u64, vm::pptr<void> out_ptr);
|
||||
error_code sys_fs_mapped_free(ppu_thread& ppu, u32 fd, vm::ptr<void> ptr);
|
||||
error_code sys_fs_truncate2(ppu_thread& ppu, u32 fd, u64 size);
|
||||
error_code sys_fs_newfs(ppu_thread& ppu, vm::cptr<char> dev_name, vm::cptr<char> file_system, s32 unk1, vm::cptr<char> str1);
|
||||
error_code sys_fs_mount(ppu_thread& ppu, vm::cptr<char> dev_name, vm::cptr<char> file_system, vm::cptr<char> path, s32 unk1, s32 prot, s32 unk3, vm::cptr<char> str1, u32 str_len);
|
||||
error_code sys_fs_newfs(ppu_thread&, vm::cptr<char> dev_name, vm::cptr<char> file_system, s32 unk1, vm::cptr<char> str1);
|
||||
error_code sys_fs_mount(ppu_thread&, vm::cptr<char> dev_name, vm::cptr<char> file_system, vm::cptr<char> path, s32 unk1, s32 prot, s32 unk3, vm::cptr<char> str1, u32 str_len);
|
||||
error_code sys_fs_unmount(ppu_thread&, vm::cptr<char> path, s32 unk1, s32 unk2);
|
||||
error_code sys_fs_get_mount_info_size(ppu_thread& ppu, vm::ptr<u64> len);
|
||||
error_code sys_fs_get_mount_info(ppu_thread& ppu, vm::ptr<CellFsMountInfo> info, u32 len, vm::ptr<u64> out_len);
|
||||
error_code sys_fs_get_mount_info_size(ppu_thread&, vm::ptr<u64> len);
|
||||
error_code sys_fs_get_mount_info(ppu_thread&, vm::ptr<CellFsMountInfo> info, u32 len, vm::ptr<u64> out_len);
|
||||
|
@ -73,6 +73,11 @@ bool vfs::mount(std::string_view vpath, std::string_view path, bool is_dir)
|
||||
list.back()->path += '/';
|
||||
if (!is_dir && list.back()->path.ends_with('/'))
|
||||
vfs_log.error("File mounted with trailing /.");
|
||||
const auto mp = lv2_fs_object::get_mp(vpath_backup);
|
||||
if (mp == &g_mp_sys_dev_usb)
|
||||
mp->is_mounted |= (1U << vpath_backup.back() - '0');
|
||||
else
|
||||
mp->is_mounted = 1U;
|
||||
vfs_log.notice("Mounted path \"%s\" to \"%s\"", vpath_backup, list.back()->path);
|
||||
return true;
|
||||
}
|
||||
@ -180,6 +185,12 @@ bool vfs::unmount(std::string_view vpath)
|
||||
};
|
||||
unmount_children(table.root, 0);
|
||||
|
||||
const auto mp = lv2_fs_object::get_mp(vpath);
|
||||
if (mp == &g_mp_sys_dev_usb)
|
||||
mp->is_mounted &= ~(1U << vpath.back() - '0');
|
||||
else
|
||||
mp->is_mounted = 0U;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user