kernel/ext2: Implement directory traversal
All checks were successful
continuous-integration/drone/pr Build is passing
All checks were successful
continuous-integration/drone/pr Build is passing
This commit is contained in:
parent
393f3dfaca
commit
6ca131f8b9
@ -7,5 +7,6 @@ target_compile_definitions(moon PRIVATE EXEC_DEBUG)
|
|||||||
target_compile_definitions(moon PRIVATE OPEN_DEBUG)
|
target_compile_definitions(moon PRIVATE OPEN_DEBUG)
|
||||||
target_compile_definitions(moon PRIVATE REAP_DEBUG)
|
target_compile_definitions(moon PRIVATE REAP_DEBUG)
|
||||||
target_compile_definitions(moon PRIVATE PCI_DEBUG)
|
target_compile_definitions(moon PRIVATE PCI_DEBUG)
|
||||||
|
target_compile_definitions(moon PRIVATE EXT2_DEBUG)
|
||||||
target_compile_definitions(moon PRIVATE DEVICE_REGISTRY_DEBUG)
|
target_compile_definitions(moon PRIVATE DEVICE_REGISTRY_DEBUG)
|
||||||
target_compile_options(moon PRIVATE -fsanitize=undefined)
|
target_compile_options(moon PRIVATE -fsanitize=undefined)
|
||||||
|
@ -25,7 +25,7 @@ namespace Ext2
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<SharedPtr<VFS::Inode>> FileSystem::find_inode_by_number(ino_t inum)
|
Result<SharedPtr<VFS::Inode>> FileSystem::find_inode_by_number(ino_t inum, bool initialize_dir_now)
|
||||||
{
|
{
|
||||||
check(inum < m_superblock.nr_inodes);
|
check(inum < m_superblock.nr_inodes);
|
||||||
|
|
||||||
@ -37,23 +37,29 @@ namespace Ext2
|
|||||||
const auto* block_group_descriptor = TRY(find_block_group_descriptor(block_group));
|
const auto* block_group_descriptor = TRY(find_block_group_descriptor(block_group));
|
||||||
check(block_group_descriptor);
|
check(block_group_descriptor);
|
||||||
|
|
||||||
// FIXME: This is determined by a field in the Superblock if the Ext2 revision >= 1.0.
|
// FIXME: Even if the inode size is bigger (Ext2::FileSystem::m_inode_size), we only read this amount. Enlarge
|
||||||
|
// the Inode structure to fit this case.
|
||||||
static constexpr usize INODE_SIZE = 128;
|
static constexpr usize INODE_SIZE = 128;
|
||||||
|
|
||||||
const u64 index = (inum - 1) % m_superblock.inodes_per_block_group;
|
const u64 index = (inum - 1) % m_superblock.inodes_per_block_group;
|
||||||
|
|
||||||
const u64 inode_address = (block_group_descriptor->inode_table_start * m_block_size) + (index * INODE_SIZE);
|
const u64 inode_address = (block_group_descriptor->inode_table_start * m_block_size) + (index * m_inode_size);
|
||||||
|
|
||||||
auto inode = TRY(adopt_shared_if_nonnull(new (std::nothrow) Ext2::Inode({}, this)));
|
auto inode = TRY(adopt_shared_if_nonnull(new (std::nothrow) Ext2::Inode({}, this)));
|
||||||
TRY(m_host_device->read((u8*)&inode->m_raw_inode, inode_address, INODE_SIZE));
|
TRY(m_host_device->read((u8*)&inode->m_raw_inode, inode_address, INODE_SIZE));
|
||||||
inode->m_type = vfs_type_from_ext2_type(inode->m_raw_inode.mode);
|
inode->m_type = vfs_type_from_ext2_type(inode->m_raw_inode.mode);
|
||||||
inode->m_inum = inum;
|
inode->m_inum = inum;
|
||||||
|
|
||||||
|
#ifdef EXT2_DEBUG
|
||||||
kdbgln("ext2: Read inode %lu with mode %#x (%#x + %#o), size %lu", inum, inode->m_raw_inode.mode,
|
kdbgln("ext2: Read inode %lu with mode %#x (%#x + %#o), size %lu", inum, inode->m_raw_inode.mode,
|
||||||
inode->m_raw_inode.mode & 0xf000, inode->mode(), inode->size());
|
inode->m_raw_inode.mode & 0xf000, inode->mode(), inode->size());
|
||||||
|
#endif
|
||||||
|
|
||||||
// TODO: Locate the inode's block group descriptor and find it in the block group's inode table.
|
m_inode_cache.try_set(inum, inode);
|
||||||
return err(ENOENT);
|
|
||||||
|
if (initialize_dir_now && inode->m_type == VFS::InodeType::Directory) TRY(inode->lazy_initialize_dir());
|
||||||
|
|
||||||
|
return (SharedPtr<VFS::Inode>)inode;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<const BlockGroupDescriptor*> FileSystem::find_block_group_descriptor(u32 index)
|
Result<const BlockGroupDescriptor*> FileSystem::find_block_group_descriptor(u32 index)
|
||||||
@ -80,20 +86,43 @@ namespace Ext2
|
|||||||
if (nread != 1024) return err(EINVAL); // Source had an invalid superblock.
|
if (nread != 1024) return err(EINVAL); // Source had an invalid superblock.
|
||||||
if (fs->m_superblock.signature != EXT2_MAGIC) return err(EINVAL); // Source had an invalid superblock.
|
if (fs->m_superblock.signature != EXT2_MAGIC) return err(EINVAL); // Source had an invalid superblock.
|
||||||
|
|
||||||
|
if (fs->m_superblock.major_version >= 1)
|
||||||
|
{
|
||||||
|
auto required = fs->m_superblock.ext_superblock.required_features;
|
||||||
|
if (required & EXT2_REQUIRED_COMPAT_D_TYPE) fs->m_dirs_have_type_field = true;
|
||||||
|
|
||||||
|
required &= ~EXT2_REQUIRED_COMPAT_D_TYPE;
|
||||||
|
|
||||||
|
if (required > 0)
|
||||||
|
{
|
||||||
|
kwarnln("ext2: File system has required features not supported by the implementation, cannot mount");
|
||||||
|
return err(EINVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs->m_uses_extended_size = true;
|
||||||
|
fs->m_inode_size = fs->m_superblock.ext_superblock.inode_size;
|
||||||
|
}
|
||||||
|
|
||||||
fs->m_host_device = host_device;
|
fs->m_host_device = host_device;
|
||||||
|
|
||||||
fs->m_block_size = 1024 << fs->m_superblock.log_block_size;
|
fs->m_block_size = 1024 << fs->m_superblock.log_block_size;
|
||||||
fs->m_block_groups = get_blocks_from_size(fs->m_superblock.nr_blocks, fs->m_superblock.blocks_per_block_group);
|
fs->m_block_groups = get_blocks_from_size(fs->m_superblock.nr_blocks, fs->m_superblock.blocks_per_block_group);
|
||||||
|
|
||||||
|
#ifdef EXT2_DEBUG
|
||||||
kdbgln("ext2: Mounting new Ext2 file system, block size=%lu, blocks=%u, inodes=%u, block group=(%u blocks, %u "
|
kdbgln("ext2: Mounting new Ext2 file system, block size=%lu, blocks=%u, inodes=%u, block group=(%u blocks, %u "
|
||||||
"inodes), %lu block groups",
|
"inodes), %lu block groups",
|
||||||
fs->m_block_size, fs->m_superblock.nr_blocks, fs->m_superblock.nr_inodes,
|
fs->m_block_size, fs->m_superblock.nr_blocks, fs->m_superblock.nr_inodes,
|
||||||
fs->m_superblock.blocks_per_block_group, fs->m_superblock.inodes_per_block_group, fs->m_block_groups);
|
fs->m_superblock.blocks_per_block_group, fs->m_superblock.inodes_per_block_group, fs->m_block_groups);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Lookup the root inode.
|
// Lookup the root inode.
|
||||||
fs->find_inode_by_number(2);
|
fs->m_root_inode = TRY(fs->find_inode_by_number(2, true));
|
||||||
|
|
||||||
// TODO: Implement basic Ext2 reading, enough to be able to mount a volume.
|
return (SharedPtr<VFS::FileSystem>)fs;
|
||||||
return err(ENOTSUP);
|
}
|
||||||
|
|
||||||
|
Result<void> FileSystem::set_mount_dir(SharedPtr<VFS::Inode> inode)
|
||||||
|
{
|
||||||
|
return m_root_inode->replace_entry(inode, "..");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#define EXT2_MAGIC 0xef53
|
#define EXT2_MAGIC 0xef53
|
||||||
|
|
||||||
|
#define EXT2_REQUIRED_COMPAT_D_TYPE 0x0002
|
||||||
|
|
||||||
namespace Ext2
|
namespace Ext2
|
||||||
{
|
{
|
||||||
struct [[gnu::packed]] Superblock
|
struct [[gnu::packed]] Superblock
|
||||||
@ -37,8 +39,27 @@ namespace Ext2
|
|||||||
|
|
||||||
u16 reserved_block_uid;
|
u16 reserved_block_uid;
|
||||||
u16 reserved_block_gid;
|
u16 reserved_block_gid;
|
||||||
// TODO: Add extended superblock fields.
|
struct
|
||||||
u8 padding[1024 - 84];
|
{
|
||||||
|
u32 first_non_reserved_inode;
|
||||||
|
u16 inode_size;
|
||||||
|
u16 this_block_group;
|
||||||
|
u32 optional_features;
|
||||||
|
u32 required_features;
|
||||||
|
u32 ro_features;
|
||||||
|
u8 fsid[16];
|
||||||
|
u8 name[16];
|
||||||
|
u8 last_mountpoint[64];
|
||||||
|
u32 compression_algs;
|
||||||
|
u8 nr_preallocated_blocks_for_files;
|
||||||
|
u8 nr_preallocated_blocks_for_dirs;
|
||||||
|
u16 unused;
|
||||||
|
u8 journal_id[16];
|
||||||
|
u32 journal_inode;
|
||||||
|
u32 journal_device;
|
||||||
|
u32 orphan_inode_head;
|
||||||
|
} ext_superblock;
|
||||||
|
u8 padding[1024 - 236];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct [[gnu::packed]] BlockGroupDescriptor
|
struct [[gnu::packed]] BlockGroupDescriptor
|
||||||
@ -77,6 +98,21 @@ namespace Ext2
|
|||||||
u32 os_specific_2[3];
|
u32 os_specific_2[3];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct [[gnu::packed]] RawDirectoryEntry
|
||||||
|
{
|
||||||
|
u32 inum;
|
||||||
|
u16 size;
|
||||||
|
union {
|
||||||
|
u16 name_length;
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
u8 name_length_low;
|
||||||
|
u8 type;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
char name[4]; // Names should be padded to a multiple of 4 bytes.
|
||||||
|
};
|
||||||
|
|
||||||
static_assert(sizeof(Superblock) == 1024);
|
static_assert(sizeof(Superblock) == 1024);
|
||||||
static_assert(sizeof(BlockGroupDescriptor) == 32);
|
static_assert(sizeof(BlockGroupDescriptor) == 32);
|
||||||
static_assert(sizeof(RawInode) == 128);
|
static_assert(sizeof(RawInode) == 128);
|
||||||
@ -111,10 +147,7 @@ namespace Ext2
|
|||||||
return err(EROFS);
|
return err(EROFS);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<void> set_mount_dir(SharedPtr<VFS::Inode>) override
|
Result<void> set_mount_dir(SharedPtr<VFS::Inode>) override;
|
||||||
{
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_readonly() const override
|
bool is_readonly() const override
|
||||||
{
|
{
|
||||||
@ -128,7 +161,7 @@ namespace Ext2
|
|||||||
return m_host_device_id;
|
return m_host_device_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<SharedPtr<VFS::Inode>> find_inode_by_number(ino_t inode);
|
Result<SharedPtr<VFS::Inode>> find_inode_by_number(ino_t inode, bool initialize_dir_now = false);
|
||||||
Result<const BlockGroupDescriptor*> find_block_group_descriptor(u32 index);
|
Result<const BlockGroupDescriptor*> find_block_group_descriptor(u32 index);
|
||||||
|
|
||||||
virtual ~FileSystem() = default;
|
virtual ~FileSystem() = default;
|
||||||
@ -146,6 +179,11 @@ namespace Ext2
|
|||||||
u64 m_block_size;
|
u64 m_block_size;
|
||||||
u64 m_block_groups;
|
u64 m_block_groups;
|
||||||
|
|
||||||
|
u32 m_inode_size { 128 };
|
||||||
|
|
||||||
|
bool m_dirs_have_type_field { false };
|
||||||
|
bool m_uses_extended_size { false };
|
||||||
|
|
||||||
// FIXME: This inode cache will keep all inodes in it alive despite having no other references to it, but we're
|
// FIXME: This inode cache will keep all inodes in it alive despite having no other references to it, but we're
|
||||||
// not worrying about that as for now the filesystem implementation is read-only.
|
// not worrying about that as for now the filesystem implementation is read-only.
|
||||||
HashMap<ino_t, SharedPtr<VFS::Inode>> m_inode_cache;
|
HashMap<ino_t, SharedPtr<VFS::Inode>> m_inode_cache;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#include "fs/ext2/Inode.h"
|
#include "fs/ext2/Inode.h"
|
||||||
|
#include <luna/String.h>
|
||||||
|
|
||||||
namespace Ext2
|
namespace Ext2
|
||||||
{
|
{
|
||||||
@ -6,6 +7,13 @@ namespace Ext2
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usize Inode::size() const
|
||||||
|
{
|
||||||
|
return (m_fs->m_uses_extended_size && (m_type == VFS::InodeType::RegularFile))
|
||||||
|
? ((u64)m_raw_inode.size_high << 32) | (u64)m_raw_inode.size_low
|
||||||
|
: m_raw_inode.size_low;
|
||||||
|
}
|
||||||
|
|
||||||
Result<usize> Inode::read(u8* buf, usize offset, usize length) const
|
Result<usize> Inode::read(u8* buf, usize offset, usize length) const
|
||||||
{
|
{
|
||||||
if (length == 0) return 0;
|
if (length == 0) return 0;
|
||||||
@ -22,6 +30,7 @@ namespace Ext2
|
|||||||
usize block_offset = (offset % block_size);
|
usize block_offset = (offset % block_size);
|
||||||
usize block = find_block(offset / block_size);
|
usize block = find_block(offset / block_size);
|
||||||
usize size_to_read = block_size - block_offset;
|
usize size_to_read = block_size - block_offset;
|
||||||
|
if (size_to_read > to_read) size_to_read = to_read;
|
||||||
|
|
||||||
usize host_offset = (block * block_size) + block_offset;
|
usize host_offset = (block * block_size) + block_offset;
|
||||||
|
|
||||||
@ -66,4 +75,91 @@ namespace Ext2
|
|||||||
|
|
||||||
return m_raw_inode.direct_pointers[index];
|
return m_raw_inode.direct_pointers[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<void> Inode::lazy_initialize_dir() const
|
||||||
|
{
|
||||||
|
check(m_type == VFS::InodeType::Directory);
|
||||||
|
|
||||||
|
const usize inode_size = size();
|
||||||
|
const usize block_size = m_fs->m_block_size;
|
||||||
|
|
||||||
|
u8* const buf = TRY(make_array<u8>(block_size));
|
||||||
|
auto guard = make_scope_guard([buf] { delete[] buf; });
|
||||||
|
|
||||||
|
m_entries.clear();
|
||||||
|
|
||||||
|
for (usize offset = 0; offset < inode_size; offset += block_size)
|
||||||
|
{
|
||||||
|
TRY(read(buf, offset, block_size));
|
||||||
|
|
||||||
|
usize dir_offset = 0;
|
||||||
|
while (dir_offset < block_size)
|
||||||
|
{
|
||||||
|
auto& entry = *(Ext2::RawDirectoryEntry*)&buf[dir_offset];
|
||||||
|
|
||||||
|
if (entry.inum != 0)
|
||||||
|
{
|
||||||
|
auto inode = TRY(m_fs->find_inode_by_number(entry.inum));
|
||||||
|
|
||||||
|
VFS::DirectoryEntry vfs_entry { inode, "" };
|
||||||
|
vfs_entry.name.adopt(entry.name,
|
||||||
|
m_fs->m_dirs_have_type_field ? entry.name_length_low : entry.name_length);
|
||||||
|
|
||||||
|
#ifdef EXT2_DEBUG
|
||||||
|
kdbgln("ext2: Read new directory entry: inum=%u, name=%s, namelen=%lu", entry.inum,
|
||||||
|
vfs_entry.name.chars(), vfs_entry.name.length());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TRY(m_entries.try_append(move(vfs_entry)));
|
||||||
|
}
|
||||||
|
|
||||||
|
dir_offset += entry.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dir_already_lazily_initialized = true;
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> Inode::replace_entry(SharedPtr<VFS::Inode> inode, const char* name)
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
if (!m_dir_already_lazily_initialized) TRY(lazy_initialize_dir());
|
||||||
|
|
||||||
|
for (auto& entry : m_entries)
|
||||||
|
{
|
||||||
|
if (!strcmp(name, entry.name.chars()))
|
||||||
|
{
|
||||||
|
entry.inode = inode;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(ENOENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SharedPtr<VFS::Inode>> Inode::find(const char* name) const
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
if (!m_dir_already_lazily_initialized) TRY(lazy_initialize_dir());
|
||||||
|
|
||||||
|
for (const auto& entry : m_entries)
|
||||||
|
{
|
||||||
|
if (!strcmp(name, entry.name.chars())) return entry.inode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(ENOENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Option<VFS::DirectoryEntry> Inode::get(usize index) const
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return {};
|
||||||
|
if (!m_dir_already_lazily_initialized)
|
||||||
|
if (lazy_initialize_dir().has_error()) return {};
|
||||||
|
|
||||||
|
if (index >= m_entries.size()) return {};
|
||||||
|
|
||||||
|
return m_entries[index];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
namespace Ext2
|
namespace Ext2
|
||||||
{
|
{
|
||||||
class Inode : public VFS::FileInode
|
class Inode : public VFS::Inode
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
VFS::InodeType type() const override
|
VFS::InodeType type() const override
|
||||||
@ -19,11 +19,7 @@ namespace Ext2
|
|||||||
return m_type;
|
return m_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
usize size() const override
|
usize size() const override;
|
||||||
{
|
|
||||||
// FIXME: If EXT2_REVISION >= 1.0, use size_high as well.
|
|
||||||
return m_raw_inode.size_low;
|
|
||||||
}
|
|
||||||
|
|
||||||
mode_t mode() const override
|
mode_t mode() const override
|
||||||
{
|
{
|
||||||
@ -85,6 +81,54 @@ namespace Ext2
|
|||||||
return err(EROFS);
|
return err(EROFS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result<SharedPtr<VFS::Inode>> find(const char*) const override;
|
||||||
|
|
||||||
|
Option<VFS::DirectoryEntry> get(usize) const override;
|
||||||
|
|
||||||
|
Result<SharedPtr<VFS::Inode>> create_file(const char*) override
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
|
||||||
|
return err(EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SharedPtr<VFS::Inode>> create_subdirectory(const char*) override
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
|
||||||
|
return err(EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> add_entry(SharedPtr<VFS::Inode>, const char*) override
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
|
||||||
|
return err(EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> replace_entry(SharedPtr<VFS::Inode>, const char*) override;
|
||||||
|
|
||||||
|
Result<void> remove_entry(const char*) override
|
||||||
|
{
|
||||||
|
if (m_type != VFS::InodeType::Directory) return err(ENOTDIR);
|
||||||
|
|
||||||
|
return err(EROFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
usize entries() const override
|
||||||
|
{
|
||||||
|
return m_entries.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool blocking() const override
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<void> lazy_initialize_dir() const;
|
||||||
|
|
||||||
|
// FIXME: Implement readlink() and device numbers.
|
||||||
|
|
||||||
Inode(Badge<FileSystem>, FileSystem* fs);
|
Inode(Badge<FileSystem>, FileSystem* fs);
|
||||||
virtual ~Inode() = default;
|
virtual ~Inode() = default;
|
||||||
|
|
||||||
@ -94,6 +138,9 @@ namespace Ext2
|
|||||||
FileSystem* m_fs;
|
FileSystem* m_fs;
|
||||||
ino_t m_inum;
|
ino_t m_inum;
|
||||||
|
|
||||||
|
mutable Vector<VFS::DirectoryEntry> m_entries;
|
||||||
|
mutable bool m_dir_already_lazily_initialized { false };
|
||||||
|
|
||||||
usize find_block(usize index) const;
|
usize find_block(usize index) const;
|
||||||
|
|
||||||
friend class FileSystem;
|
friend class FileSystem;
|
||||||
|
@ -25,6 +25,16 @@ template <usize Size> class StaticString
|
|||||||
m_length = length;
|
m_length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void adopt(const char* string, usize length)
|
||||||
|
{
|
||||||
|
if (length > Size) length = Size;
|
||||||
|
|
||||||
|
memcpy(m_buffer, string, length);
|
||||||
|
|
||||||
|
m_buffer[length] = 0;
|
||||||
|
m_length = length;
|
||||||
|
}
|
||||||
|
|
||||||
void adopt(StringView string)
|
void adopt(StringView string)
|
||||||
{
|
{
|
||||||
usize length = strlcpy(m_buffer, string.chars(),
|
usize length = strlcpy(m_buffer, string.chars(),
|
||||||
|
Loading…
Reference in New Issue
Block a user