mirror of
https://github.com/holub/mame
synced 2025-04-21 07:52:35 +03:00
prodos: Start of read support
This commit is contained in:
parent
0a8757673f
commit
ba6e0b72a7
@ -62,7 +62,7 @@ bool fs_prodos::can_format() const
|
||||
|
||||
bool fs_prodos::can_read() const
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fs_prodos::can_write() const
|
||||
@ -79,7 +79,12 @@ std::vector<fs_meta_description> fs_prodos::volume_meta_description() const
|
||||
{
|
||||
std::vector<fs_meta_description> res;
|
||||
res.emplace_back(fs_meta_description(fs_meta_name::name, fs_meta_type::string, "UNTITLED", false, [](const fs_meta &m) { std::string n = std::get<std::string>(m); return n.size() <= 15; }, "Volume name, up to 15 characters"));
|
||||
res.emplace_back(fs_meta_description(fs_meta_name::os_version, fs_meta_type::number, 5, false, [](const fs_meta &m) { return std::get<uint64_t>(m) <= 255; }, "Creator OS version"));
|
||||
res.emplace_back(fs_meta_description(fs_meta_name::os_minimum_version, fs_meta_type::number, 5, false, [](const fs_meta &m) { return std::get<uint64_t>(m) <= 255; }, "Minimum OS version"));
|
||||
|
||||
auto now = util::arbitrary_datetime::now();
|
||||
res.emplace_back(fs_meta_description(fs_meta_name::creation_date, fs_meta_type::date, now, false, nullptr, "Creation time"));
|
||||
res.emplace_back(fs_meta_description(fs_meta_name::modification_date, fs_meta_type::date, now, false, nullptr, "Modification time"));
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -100,6 +105,10 @@ void fs_prodos::impl::format(const fs_meta_data &meta)
|
||||
std::string volume_name = std::get<std::string>(meta.find(fs_meta_name::name)->second);
|
||||
u32 blocks = m_blockdev.block_count();
|
||||
|
||||
// Maximum usable partition size = 32M - 512 bytes (65535 blocks)
|
||||
if(blocks >= 0x10000)
|
||||
blocks = 0xffff;
|
||||
|
||||
m_blockdev.get(0).copy(0x000, boot, 0x200); // Standard ProDOS boot sector as written by a 2gs
|
||||
m_blockdev.get(1).fill(0x00); // No SOS boot sector
|
||||
|
||||
@ -131,11 +140,101 @@ void fs_prodos::impl::format(const fs_meta_data &meta)
|
||||
kblk4.w16l(0x00, 0x0004); // Backwards block pointer of the fourth volume block
|
||||
kblk4.w16l(0x02, 0x0000); // Forwards block pointer of the fourth volume block (null)
|
||||
|
||||
auto fmap = m_blockdev.get(6);
|
||||
u8 *fdata = fmap.data();
|
||||
// Mark blocks 7 to max as free
|
||||
for(u32 i = 7; i != blocks; i++)
|
||||
fdata[i >> 3] |= 0x80 >> (i & 7);
|
||||
u32 fmap_block_count = (blocks + 4095) / 4096;
|
||||
u32 first_free_block = 6 + fmap_block_count;
|
||||
|
||||
// Mark blocks from first_free_block to blocks-1 (the last one) as free
|
||||
for(u32 i = 0; i != fmap_block_count; i++) {
|
||||
auto fmap = m_blockdev.get(6 + i);
|
||||
u8 *fdata = fmap.data();
|
||||
u32 start = i ? 0 : first_free_block;
|
||||
u32 end = i != fmap_block_count - 1 ? 4095 : (blocks - 1) & 4095;
|
||||
end += 1;
|
||||
u32 sb = start >> 3;
|
||||
u32 si = start & 7;
|
||||
u32 eb = end >> 3;
|
||||
u32 ei = end & 7;
|
||||
if(sb == eb)
|
||||
fdata[sb] = (0xff >> si) & ~(0xff >> ei);
|
||||
else {
|
||||
fdata[sb] = 0xff >> si;
|
||||
if(eb != 512)
|
||||
fdata[eb] = ~(0xff >> ei);
|
||||
if(eb - sb > 1)
|
||||
memset(fdata+sb, 0xff, eb-sb-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs_prodos::impl::impl(fsblk_t &blockdev) : filesystem_t(blockdev, 512), m_root(true)
|
||||
{
|
||||
}
|
||||
|
||||
util::arbitrary_datetime fs_prodos::impl::prodos_to_dt(u32 date)
|
||||
{
|
||||
util::arbitrary_datetime dt;
|
||||
dt.second = 0;
|
||||
dt.minute = ((date >> 16) & 0x3f);
|
||||
dt.hour = ((date >> 24) & 0x1f);
|
||||
dt.day_of_month = ((date >> 0) & 0x1f);
|
||||
dt.month = ((date >> 5) & 0x0f) + 1;
|
||||
dt.year = ((date >> 9) & 0x7f) + 1900;
|
||||
if (dt.year <= 1949)
|
||||
dt.year += 100;
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
fs_meta_data fs_prodos::impl::metadata()
|
||||
{
|
||||
fs_meta_data res;
|
||||
auto bdir = m_blockdev.get(2);
|
||||
int len = bdir.r8(0x04) & 0xf;
|
||||
res[fs_meta_name::name] = bdir.rstr(0x05, len);
|
||||
res[fs_meta_name::os_version] = uint64_t(bdir.r8(0x20));
|
||||
res[fs_meta_name::os_minimum_version] = uint64_t(bdir.r8(0x21));
|
||||
res[fs_meta_name::creation_date] = prodos_to_dt(bdir.r32l(0x1c));
|
||||
res[fs_meta_name::modification_date] = prodos_to_dt(bdir.r32l(0x16));
|
||||
return res;
|
||||
}
|
||||
|
||||
filesystem_t::dir_t fs_prodos::impl::root()
|
||||
{
|
||||
if(!m_root)
|
||||
m_root = new root_dir(*this);
|
||||
return m_root.strong();
|
||||
}
|
||||
|
||||
void fs_prodos::impl::drop_root_ref()
|
||||
{
|
||||
m_root = nullptr;
|
||||
}
|
||||
|
||||
|
||||
void fs_prodos::impl::root_dir::drop_weak_references()
|
||||
{
|
||||
m_fs.drop_root_ref();
|
||||
}
|
||||
|
||||
fs_meta_data fs_prodos::impl::root_dir::metadata()
|
||||
{
|
||||
return fs_meta_data();
|
||||
}
|
||||
|
||||
std::vector<fs_dir_entry> fs_prodos::impl::root_dir::contents()
|
||||
{
|
||||
std::vector<fs_dir_entry> res;
|
||||
return res;
|
||||
}
|
||||
|
||||
filesystem_t::file_t fs_prodos::impl::root_dir::file_get(uint64_t key)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
filesystem_t::dir_t fs_prodos::impl::root_dir::dir_get(uint64_t key)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const filesystem_manager_type FS_PRODOS = &filesystem_manager_creator<fs_prodos>;;
|
||||
|
@ -14,13 +14,38 @@ class fs_prodos : public filesystem_manager_t {
|
||||
public:
|
||||
class impl : public filesystem_t {
|
||||
public:
|
||||
impl(fsblk_t &blockdev) : filesystem_t(blockdev, 512) {}
|
||||
class root_dir : public idir_t {
|
||||
public:
|
||||
root_dir(impl &i) : m_fs(i) {}
|
||||
virtual ~root_dir() = default;
|
||||
|
||||
virtual void drop_weak_references() override;
|
||||
|
||||
virtual fs_meta_data metadata() override;
|
||||
virtual std::vector<fs_dir_entry> contents() override;
|
||||
virtual file_t file_get(uint64_t key) override;
|
||||
virtual dir_t dir_get(uint64_t key) override;
|
||||
|
||||
private:
|
||||
impl &m_fs;
|
||||
};
|
||||
|
||||
impl(fsblk_t &blockdev);
|
||||
virtual ~impl() = default;
|
||||
|
||||
virtual void format(const fs_meta_data &meta) override;
|
||||
|
||||
virtual fs_meta_data metadata() override;
|
||||
virtual dir_t root() override;
|
||||
|
||||
void drop_root_ref();
|
||||
|
||||
static util::arbitrary_datetime prodos_to_dt(u32 date);
|
||||
|
||||
private:
|
||||
static const u8 boot[512];
|
||||
|
||||
dir_t m_root;
|
||||
};
|
||||
|
||||
fs_prodos() : filesystem_manager_t() {}
|
||||
|
@ -270,6 +270,8 @@ const char *fs_meta_get_name(fs_meta_name name)
|
||||
case fs_meta_name::modification_date: return "modification_date";
|
||||
case fs_meta_name::name: return "name";
|
||||
case fs_meta_name::size_in_blocks: return "size_in_blocks";
|
||||
case fs_meta_name::os_version: return "os_version";
|
||||
case fs_meta_name::os_minimum_version: return "os_minimum_version";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@ -280,7 +282,12 @@ std::string fs_meta_to_string(fs_meta_type type, const fs_meta &m)
|
||||
case fs_meta_type::string: return std::get<std::string>(m);
|
||||
case fs_meta_type::number: return util::string_format("0x%x", std::get<uint64_t>(m));
|
||||
case fs_meta_type::flag: return std::get<bool>(m) ? "t" : "f";
|
||||
case fs_meta_type::date: abort();
|
||||
case fs_meta_type::date: {
|
||||
auto dt = std::get<util::arbitrary_datetime>(m);
|
||||
return util::string_format("%04d-%02d-%02d %02d:%02d:%02d",
|
||||
dt.year, dt.month, dt.day_of_month,
|
||||
dt.hour, dt.minute, dt.second);
|
||||
}
|
||||
}
|
||||
return std::string("");
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "flopimg.h"
|
||||
#include "timeconv.h"
|
||||
|
||||
#include <variant>
|
||||
#include <unordered_map>
|
||||
@ -18,17 +19,19 @@ enum class fs_meta_name {
|
||||
length,
|
||||
loading_address,
|
||||
locked,
|
||||
sequential,
|
||||
modification_date,
|
||||
name,
|
||||
os_minimum_version,
|
||||
os_version,
|
||||
sequential,
|
||||
size_in_blocks,
|
||||
};
|
||||
|
||||
enum class fs_meta_type {
|
||||
string,
|
||||
number,
|
||||
date,
|
||||
flag,
|
||||
number,
|
||||
string,
|
||||
};
|
||||
|
||||
enum class fs_dir_entry_type {
|
||||
@ -37,7 +40,7 @@ enum class fs_dir_entry_type {
|
||||
system_file,
|
||||
};
|
||||
|
||||
using fs_meta = std::variant<std::string, uint64_t, bool>;
|
||||
using fs_meta = std::variant<std::string, uint64_t, bool, util::arbitrary_datetime>;
|
||||
using fs_meta_data = std::unordered_map<fs_meta_name, fs_meta>;
|
||||
|
||||
const char *fs_meta_get_name(fs_meta_name name);
|
||||
|
@ -32,6 +32,23 @@ std::chrono::system_clock::duration system_clock_adjustment(calculate_system_clo
|
||||
IMPLEMENTATION
|
||||
***************************************************************************/
|
||||
|
||||
arbitrary_datetime arbitrary_datetime::now()
|
||||
{
|
||||
time_t sec;
|
||||
time(&sec);
|
||||
auto t = *localtime(&sec);
|
||||
|
||||
arbitrary_datetime dt;
|
||||
dt.year = t.tm_year + 1900;
|
||||
dt.month = t.tm_mon + 1;
|
||||
dt.day_of_month = t.tm_mday;
|
||||
dt.hour = t.tm_hour;
|
||||
dt.minute = t.tm_min;
|
||||
dt.second = t.tm_sec;
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
static std::chrono::system_clock::duration calculate_system_clock_adjustment()
|
||||
{
|
||||
constexpr auto days_in_year(365);
|
||||
|
@ -48,6 +48,8 @@ struct arbitrary_datetime
|
||||
int hour; // hour (0-23)
|
||||
int minute; // minute (0-59)
|
||||
int second; // second (0-59)
|
||||
|
||||
static struct arbitrary_datetime now();
|
||||
};
|
||||
|
||||
|
||||
|
@ -1,12 +1,8 @@
|
||||
// license:BSD-3-Clause
|
||||
// copyright-holders:Miodrag Milanovic
|
||||
// copyright-holders:Olivier Galibert
|
||||
/***************************************************************************
|
||||
|
||||
main.c
|
||||
|
||||
Floptool command line front end
|
||||
|
||||
20/07/2011 Initial version by Miodrag Milanovic
|
||||
(Floppy) image command-line manager
|
||||
|
||||
***************************************************************************/
|
||||
|
||||
@ -274,11 +270,11 @@ static const fs_info *find_fs_by_name(const char *name)
|
||||
static void display_usage()
|
||||
{
|
||||
fprintf(stderr, "Usage: \n");
|
||||
fprintf(stderr, " floptool.exe identify <inputfile> [<inputfile> ...]\n");
|
||||
fprintf(stderr, " floptool.exe convert [input_format|auto] output_format <inputfile> <outputfile>\n");
|
||||
fprintf(stderr, " floptool.exe create output_format filesystem <outputfile>\n");
|
||||
fprintf(stderr, " floptool.exe dir input_format filesystem <image>\n");
|
||||
fprintf(stderr, " floptool.exe read input_format filesystem <image> <path> <outputfile>\n");
|
||||
fprintf(stderr, " floptool.exe identify <inputfile> [<inputfile> ...] -- Identify a floppy image format\n");
|
||||
fprintf(stderr, " floptool.exe convert [input_format|auto] output_format <inputfile> <outputfile> -- Convert a floppy image\n");
|
||||
fprintf(stderr, " floptool.exe flopcreate output_format filesystem <outputfile> -- Create a preformatted floppy image\n");
|
||||
fprintf(stderr, " floptool.exe flopdir input_format filesystem <image> -- List the contents of a floppy image\n");
|
||||
fprintf(stderr, " floptool.exe flopread input_format filesystem <image> <path> <outputfile> -- Extract a file from a floppy image\n");
|
||||
}
|
||||
|
||||
static void display_formats()
|
||||
@ -295,7 +291,7 @@ static void display_formats()
|
||||
sk = sz;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Supported formats:\n\n");
|
||||
fprintf(stderr, "Supported floppy formats:\n\n");
|
||||
for(const auto &e : formats_by_category)
|
||||
if(!e.second.empty()) {
|
||||
fprintf(stderr, "%s:\n", e.first.c_str());
|
||||
@ -304,7 +300,7 @@ static void display_formats()
|
||||
}
|
||||
|
||||
fprintf(stderr, "\n\n");
|
||||
fprintf(stderr, "Supported filesystems:\n\n");
|
||||
fprintf(stderr, "Supported floppy filesystems:\n\n");
|
||||
for(const auto &e : fs_by_category)
|
||||
if(!e.second.empty()) {
|
||||
fprintf(stderr, "%s:\n", e.first.c_str());
|
||||
@ -548,61 +544,12 @@ static void dir_scan(u32 depth, filesystem_t::dir_t dir, std::vector<std::vector
|
||||
}
|
||||
}
|
||||
|
||||
static int dir(int argc, char *argv[])
|
||||
static int generic_dir(const filesystem_manager_t *fm, fsblk_t &blockdev)
|
||||
{
|
||||
if (argc!=5) {
|
||||
fprintf(stderr, "Incorrect number of arguments.\n\n");
|
||||
display_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto format = find_format_by_name(argv[2]);
|
||||
if(!format) {
|
||||
fprintf(stderr, "Error: Format '%s' unknown\n", argv[3]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto fs = find_fs_by_name(argv[3]);
|
||||
if(!fs) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' unknown\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!fs->m_manager || !fs->m_manager->can_read()) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' does not implement reading\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char msg[4096];
|
||||
sprintf(msg, "Error opening %s for reading", argv[4]);
|
||||
FILE *f = fopen(argv[4], "rb");
|
||||
if (!f) {
|
||||
perror(msg);
|
||||
return 1;
|
||||
}
|
||||
io_generic io;
|
||||
io.file = f;
|
||||
io.procs = &stdio_ioprocs_noclose;
|
||||
io.filler = 0xff;
|
||||
|
||||
floppy_image image(84, 2, floppy_image::FF_UNKNOWN);
|
||||
if(!format->load(&io, floppy_image::FF_UNKNOWN, variants, &image)) {
|
||||
fprintf(stderr, "Error: parsing input file as '%s' failed\n", format->name());
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<u8> img;
|
||||
auto iog = ram_open(img);
|
||||
auto load_format = fs->m_type();
|
||||
load_format->save(iog, variants, &image);
|
||||
delete load_format;
|
||||
delete iog;
|
||||
|
||||
fsblk_vec_t blockdev(img);
|
||||
auto load_fs = fs->m_manager->mount(blockdev);
|
||||
auto vmetad = fs->m_manager->volume_meta_description();
|
||||
auto fmetad = fs->m_manager->file_meta_description();
|
||||
auto dmetad = fs->m_manager->directory_meta_description();
|
||||
auto load_fs = fm->mount(blockdev);
|
||||
auto vmetad = fm->volume_meta_description();
|
||||
auto fmetad = fm->file_meta_description();
|
||||
auto dmetad = fm->directory_meta_description();
|
||||
|
||||
auto vmeta = load_fs->metadata();
|
||||
if(!vmeta.empty()) {
|
||||
@ -653,10 +600,9 @@ static int dir(int argc, char *argv[])
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int doread(int argc, char *argv[])
|
||||
static int flopdir(int argc, char *argv[])
|
||||
{
|
||||
if (argc!=7) {
|
||||
if (argc!=5) {
|
||||
fprintf(stderr, "Incorrect number of arguments.\n\n");
|
||||
display_usage();
|
||||
return 1;
|
||||
@ -704,6 +650,102 @@ static int doread(int argc, char *argv[])
|
||||
delete load_format;
|
||||
delete iog;
|
||||
|
||||
fsblk_vec_t blockdev(img);
|
||||
return generic_dir(fs->m_manager, blockdev);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Should use chd&friends instead, but one thing at a time
|
||||
|
||||
static int hddir(int argc, char *argv[])
|
||||
{
|
||||
if (argc!=4) {
|
||||
fprintf(stderr, "Incorrect number of arguments.\n\n");
|
||||
display_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto fs = find_fs_by_name(argv[2]);
|
||||
if(!fs) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' unknown\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!fs->m_manager || !fs->m_manager->can_read()) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' does not implement reading\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char msg[4096];
|
||||
sprintf(msg, "Error opening %s for reading", argv[3]);
|
||||
FILE *f = fopen(argv[3], "rb");
|
||||
if (!f) {
|
||||
perror(msg);
|
||||
return 1;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
size_t size = ftell(f);
|
||||
rewind(f);
|
||||
std::vector<u8> img(size);
|
||||
fread(img.data(), size, 1, f);
|
||||
fclose(f);
|
||||
|
||||
fsblk_vec_t blockdev(img);
|
||||
return generic_dir(fs->m_manager, blockdev);
|
||||
}
|
||||
|
||||
|
||||
static int flopread(int argc, char *argv[])
|
||||
{
|
||||
if (argc!=7) {
|
||||
fprintf(stderr, "Incorrect number of arguments.\n\n");
|
||||
display_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto format = find_format_by_name(argv[2]);
|
||||
if(!format) {
|
||||
fprintf(stderr, "Error: Format '%s' unknown\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto fs = find_fs_by_name(argv[3]);
|
||||
if(!fs) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' unknown\n", argv[3]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!fs->m_manager || !fs->m_manager->can_read()) {
|
||||
fprintf(stderr, "Error: Filesystem '%s' does not implement reading\n", argv[2]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char msg[4096];
|
||||
sprintf(msg, "Error opening %s for reading", argv[4]);
|
||||
FILE *f = fopen(argv[4], "rb");
|
||||
if (!f) {
|
||||
perror(msg);
|
||||
return 1;
|
||||
}
|
||||
io_generic io;
|
||||
io.file = f;
|
||||
io.procs = &stdio_ioprocs_noclose;
|
||||
io.filler = 0xff;
|
||||
|
||||
floppy_image image(84, 2, floppy_image::FF_UNKNOWN);
|
||||
if(!format->load(&io, floppy_image::FF_UNKNOWN, variants, &image)) {
|
||||
fprintf(stderr, "Error: parsing input file as '%s' failed\n", format->name());
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<u8> img;
|
||||
auto iog = ram_open(img);
|
||||
auto load_format = fs->m_type();
|
||||
load_format->save(iog, variants, &image);
|
||||
delete load_format;
|
||||
delete iog;
|
||||
|
||||
fsblk_vec_t blockdev(img);
|
||||
auto load_fs = fs->m_manager->mount(blockdev);
|
||||
|
||||
@ -777,19 +819,26 @@ int CLIB_DECL main(int argc, char *argv[])
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!core_stricmp("identify", argv[1]))
|
||||
return identify(argc, argv);
|
||||
else if (!core_stricmp("convert", argv[1]))
|
||||
return convert(argc, argv);
|
||||
else if (!core_stricmp("create", argv[1]))
|
||||
return create(argc, argv);
|
||||
else if (!core_stricmp("dir", argv[1]))
|
||||
return dir(argc, argv);
|
||||
else if (!core_stricmp("read", argv[1]))
|
||||
return doread(argc, argv);
|
||||
else {
|
||||
fprintf(stderr, "Unknown command '%s'\n\n", argv[1]);
|
||||
display_usage();
|
||||
try {
|
||||
if (!core_stricmp("identify", argv[1]))
|
||||
return identify(argc, argv);
|
||||
else if (!core_stricmp("convert", argv[1]))
|
||||
return convert(argc, argv);
|
||||
else if (!core_stricmp("create", argv[1]))
|
||||
return create(argc, argv);
|
||||
else if (!core_stricmp("flopdir", argv[1]))
|
||||
return flopdir(argc, argv);
|
||||
else if (!core_stricmp("flopread", argv[1]))
|
||||
return flopread(argc, argv);
|
||||
else if (!core_stricmp("hddir", argv[1]))
|
||||
return hddir(argc, argv);
|
||||
else {
|
||||
fprintf(stderr, "Unknown command '%s'\n\n", argv[1]);
|
||||
display_usage();
|
||||
return 1;
|
||||
}
|
||||
} catch(const emu_fatalerror &err) {
|
||||
fprintf(stderr, "Error: %s", err.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user