mirror of
https://github.com/Atmosphere-NX/Atmosphere.git
synced 2025-04-22 12:34:47 +00:00
fs: add substorage, rom path tool
This commit is contained in:
parent
f39781ec11
commit
a397853e3b
7 changed files with 509 additions and 2 deletions
|
@ -22,6 +22,7 @@
|
|||
#include "fs/fsa/fs_registrar.hpp"
|
||||
#include "fs/fs_remote_filesystem.hpp"
|
||||
#include "fs/fs_istorage.hpp"
|
||||
#include "fs/fs_substorage.hpp"
|
||||
#include "fs/fs_remote_storage.hpp"
|
||||
#include "fs/fs_file_storage.hpp"
|
||||
#include "fs/fs_query_range.hpp"
|
||||
|
@ -29,6 +30,7 @@
|
|||
#include "fs/fs_mount.hpp"
|
||||
#include "fs/fs_path_tool.hpp"
|
||||
#include "fs/fs_path_utils.hpp"
|
||||
#include "fs/fs_rom_path_tool.hpp"
|
||||
#include "fs/fs_content_storage.hpp"
|
||||
#include "fs/fs_game_card.hpp"
|
||||
#include "fs/fs_sd_card.hpp"
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace ams::fs {
|
|||
constexpr inline char Dot = '.';
|
||||
constexpr inline char NullTerminator = '\x00';
|
||||
|
||||
constexpr inline char UnsupportedDirectorySeparator = '/';
|
||||
constexpr inline char UnsupportedDirectorySeparator = '\\';
|
||||
}
|
||||
|
||||
class PathTool {
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "fs_rom_types.hpp"
|
||||
|
||||
namespace ams::fs {
|
||||
|
||||
namespace RomPathTool {
|
||||
|
||||
constexpr inline u32 MaxPathLength = 0x300;
|
||||
|
||||
struct RomEntryName {
|
||||
size_t length;
|
||||
const RomPathChar *path;
|
||||
};
|
||||
static_assert(std::is_pod<RomEntryName>::value);
|
||||
|
||||
constexpr void InitializeRomEntryName(RomEntryName *entry) {
|
||||
AMS_ABORT_UNLESS(entry != nullptr);
|
||||
entry->length = 0;
|
||||
}
|
||||
|
||||
constexpr inline bool IsSeparator(RomPathChar c) {
|
||||
return c == RomStringTraits::DirectorySeparator;
|
||||
}
|
||||
|
||||
constexpr inline bool IsNullTerminator(RomPathChar c) {
|
||||
return c == RomStringTraits::NullTerminator;
|
||||
}
|
||||
|
||||
constexpr inline bool IsDot(RomPathChar c) {
|
||||
return c == RomStringTraits::Dot;
|
||||
}
|
||||
|
||||
constexpr inline bool IsCurrentDirectory(const RomEntryName &name) {
|
||||
return name.length == 1 && IsDot(name.path[0]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsCurrentDirectory(const RomPathChar *p, size_t length) {
|
||||
AMS_ABORT_UNLESS(p != nullptr);
|
||||
return length == 1 && IsDot(p[0]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsCurrentDirectory(const RomPathChar *p) {
|
||||
AMS_ABORT_UNLESS(p != nullptr);
|
||||
return IsDot(p[0]) && IsNullTerminator(p[1]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsParentDirectory(const RomEntryName &name) {
|
||||
return name.length == 2 && IsDot(name.path[0]) && IsDot(name.path[1]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsParentDirectory(const RomPathChar *p) {
|
||||
AMS_ABORT_UNLESS(p != nullptr);
|
||||
return IsDot(p[0]) && IsDot(p[1]) && IsNullTerminator(p[2]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsParentDirectory(const RomPathChar *p, size_t length) {
|
||||
AMS_ABORT_UNLESS(p != nullptr);
|
||||
return length == 2 && IsDot(p[0]) && IsDot(p[1]);
|
||||
}
|
||||
|
||||
constexpr inline bool IsEqualPath(const RomPathChar *lhs, const RomPathChar *rhs, size_t length) {
|
||||
AMS_ABORT_UNLESS(lhs != nullptr);
|
||||
AMS_ABORT_UNLESS(rhs != nullptr);
|
||||
return std::strncmp(lhs, rhs, length) == 0;
|
||||
}
|
||||
|
||||
constexpr inline bool IsEqualName(const RomEntryName &lhs, const RomPathChar *rhs) {
|
||||
AMS_ABORT_UNLESS(rhs != nullptr);
|
||||
if (strnlen(rhs, MaxPathLength) != lhs.length) {
|
||||
return false;
|
||||
}
|
||||
return IsEqualPath(lhs.path, rhs, lhs.length);
|
||||
}
|
||||
|
||||
constexpr inline bool IsEqualName(const RomEntryName &lhs, const RomEntryName &rhs) {
|
||||
if (lhs.length != rhs.length) {
|
||||
return false;
|
||||
}
|
||||
return IsEqualPath(lhs.path, rhs.path, lhs.length);
|
||||
}
|
||||
|
||||
Result GetParentDirectoryName(RomEntryName *out, const RomEntryName &cur, const RomPathChar *p);
|
||||
|
||||
class PathParser {
|
||||
private:
|
||||
const RomPathChar *prev_path_start;
|
||||
const RomPathChar *prev_path_end;
|
||||
const RomPathChar *next_path;
|
||||
bool finished;
|
||||
public:
|
||||
constexpr PathParser() : prev_path_start(), prev_path_end(), next_path(), finished() { /* ... */ }
|
||||
|
||||
Result Initialize(const RomPathChar *path);
|
||||
void Finalize();
|
||||
|
||||
bool IsFinished() const;
|
||||
bool IsDirectoryPath() const;
|
||||
|
||||
Result GetAsDirectoryName(RomEntryName *out) const;
|
||||
Result GetAsFileName(RomEntryName *out) const;
|
||||
|
||||
Result GetNextDirectoryName(RomEntryName *out);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "fs_common.hpp"
|
||||
|
||||
namespace ams::fs {
|
||||
|
||||
using RomPathChar = char;
|
||||
using RomFileId = s32;
|
||||
using RomDirectoryId = s32;
|
||||
|
||||
struct RomFileSystemInformation {
|
||||
s64 size;
|
||||
s64 directory_bucket_offset;
|
||||
s64 directory_bucket_size;
|
||||
s64 directory_entry_offset;
|
||||
s64 directory_entry_size;
|
||||
s64 file_bucket_offset;
|
||||
s64 file_bucket_size;
|
||||
s64 file_entry_offset;
|
||||
s64 file_entry_size;
|
||||
s64 body_offset;
|
||||
};
|
||||
static_assert(std::is_pod<RomFileSystemInformation>::value);
|
||||
static_assert(sizeof(RomFileSystemInformation) == 0x50);
|
||||
|
||||
struct RomDirectoryInfo {
|
||||
/* ... */
|
||||
};
|
||||
static_assert(std::is_pod<RomDirectoryInfo>::value);
|
||||
|
||||
struct RomFileInfo {
|
||||
s64 offset;
|
||||
s64 size;
|
||||
};
|
||||
static_assert(std::is_pod<RomFileInfo>::value);
|
||||
|
||||
namespace RomStringTraits {
|
||||
|
||||
constexpr inline char DirectorySeparator = '/';
|
||||
constexpr inline char NullTerminator = '\x00';
|
||||
constexpr inline char Dot = '.';
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "impl/fs_newable.hpp"
|
||||
#include "fs_istorage.hpp"
|
||||
|
||||
namespace ams::fs {
|
||||
|
||||
class SubStorage : public ::ams::fs::IStorage, public ::ams::fs::impl::Newable {
|
||||
private:
|
||||
std::shared_ptr<IStorage> shared_base_storage;
|
||||
fs::IStorage *base_storage;
|
||||
s64 offset;
|
||||
s64 size;
|
||||
bool resizable;
|
||||
private:
|
||||
constexpr bool IsValid() const {
|
||||
return this->base_storage != nullptr;
|
||||
}
|
||||
public:
|
||||
SubStorage() : shared_base_storage(), base_storage(nullptr), offset(0), size(0), resizable(false) { /* ... */ }
|
||||
|
||||
SubStorage(const SubStorage &rhs) : shared_base_storage(), base_storage(rhs.base_storage), offset(rhs.offset), size(rhs.size), resizable(rhs.resizable) { /* ... */}
|
||||
SubStorage &operator=(const SubStorage &rhs) {
|
||||
if (this != std::addressof(rhs)) {
|
||||
this->base_storage = rhs.base_storage;
|
||||
this->offset = rhs.offset;
|
||||
this->size = rhs.size;
|
||||
this->resizable = rhs.resizable;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
SubStorage(IStorage *storage, s64 o, s64 sz) : shared_base_storage(), base_storage(storage), offset(o), size(sz) {
|
||||
AMS_ABORT_UNLESS(this->IsValid());
|
||||
AMS_ABORT_UNLESS(this->offset >= 0);
|
||||
AMS_ABORT_UNLESS(this->size >= 0);
|
||||
}
|
||||
|
||||
SubStorage(std::shared_ptr<IStorage> storage, s64 o, s64 sz) : shared_base_storage(storage), base_storage(storage.get()), offset(o), size(sz) {
|
||||
AMS_ABORT_UNLESS(this->IsValid());
|
||||
AMS_ABORT_UNLESS(this->offset >= 0);
|
||||
AMS_ABORT_UNLESS(this->size >= 0);
|
||||
}
|
||||
|
||||
SubStorage(SubStorage *sub, s64 o, s64 sz) : shared_base_storage(), base_storage(sub->base_storage), offset(o + sub->offset), size(sz) {
|
||||
AMS_ABORT_UNLESS(this->IsValid());
|
||||
AMS_ABORT_UNLESS(this->offset >= 0);
|
||||
AMS_ABORT_UNLESS(this->size >= 0);
|
||||
AMS_ABORT_UNLESS(sub->size >= o + sz);
|
||||
}
|
||||
|
||||
public:
|
||||
void SetResizable(bool rsz) {
|
||||
this->resizable = rsz;
|
||||
}
|
||||
public:
|
||||
virtual Result Read(s64 offset, void *buffer, size_t size) override {
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
R_UNLESS(size != 0, ResultSuccess());
|
||||
R_UNLESS(buffer != nullptr, fs::ResultNullptrArgument());
|
||||
R_UNLESS(IStorage::IsRangeValid(offset, size, this->size), fs::ResultOutOfRange());
|
||||
return this->base_storage->Read(this->offset + offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual Result Write(s64 offset, const void *buffer, size_t size) override{
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
R_UNLESS(size != 0, ResultSuccess());
|
||||
R_UNLESS(buffer != nullptr, fs::ResultNullptrArgument());
|
||||
R_UNLESS(IStorage::IsRangeValid(offset, size, this->size), fs::ResultOutOfRange());
|
||||
return this->base_storage->Write(this->offset + offset, buffer, size);
|
||||
}
|
||||
|
||||
virtual Result Flush() override {
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
return this->base_storage->Flush();
|
||||
}
|
||||
|
||||
virtual Result SetSize(s64 size) override {
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
R_UNLESS(this->resizable, fs::ResultUnsupportedOperation());
|
||||
R_UNLESS(IStorage::IsOffsetAndSizeValid(this->offset, size), fs::ResultInvalidSize());
|
||||
|
||||
s64 cur_size;
|
||||
R_TRY(this->base_storage->GetSize(std::addressof(cur_size)));
|
||||
|
||||
R_UNLESS(cur_size == this->offset + this->size, fs::ResultUnsupportedOperation());
|
||||
|
||||
R_TRY(this->base_storage->SetSize(this->offset + size));
|
||||
|
||||
this->size = size;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
virtual Result GetSize(s64 *out) override {
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
*out = this->size;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
virtual Result OperateRange(void *dst, size_t dst_size, OperationId op_id, s64 offset, s64 size, const void *src, size_t src_size) override {
|
||||
R_UNLESS(this->IsValid(), fs::ResultNotInitialized());
|
||||
R_UNLESS(size != 0, ResultSuccess());
|
||||
R_UNLESS(IStorage::IsOffsetAndSizeValid(offset, size), fs::ResultOutOfRange());
|
||||
return this->base_storage->OperateRange(dst, dst_size, op_id, this->offset + offset, size, src, src_size);
|
||||
}
|
||||
|
||||
using IStorage::OperateRange;
|
||||
};
|
||||
|
||||
}
|
194
libraries/libstratosphere/source/fs/fs_rom_path_tool.cpp
Normal file
194
libraries/libstratosphere/source/fs/fs_rom_path_tool.cpp
Normal file
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020 Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <stratosphere.hpp>
|
||||
|
||||
namespace ams::fs::RomPathTool {
|
||||
|
||||
Result PathParser::Initialize(const RomPathChar *path) {
|
||||
AMS_ABORT_UNLESS(path != nullptr);
|
||||
|
||||
/* Require paths start with a separator, and skip repeated separators. */
|
||||
R_UNLESS(IsSeparator(path[0]), fs::ResultDbmInvalidPathFormat());
|
||||
while (IsSeparator(path[1])) {
|
||||
path++;
|
||||
}
|
||||
|
||||
this->prev_path_start = path;
|
||||
this->prev_path_end = path;
|
||||
this->next_path = path + 1;
|
||||
while (IsSeparator(this->next_path[0])) {
|
||||
this->next_path++;
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
void PathParser::Finalize() {
|
||||
this->prev_path_start = nullptr;
|
||||
this->prev_path_end = nullptr;
|
||||
this->next_path = nullptr;
|
||||
this->finished = false;
|
||||
}
|
||||
|
||||
bool PathParser::IsFinished() const {
|
||||
return this->finished;
|
||||
}
|
||||
|
||||
bool PathParser::IsDirectoryPath() const {
|
||||
AMS_ASSERT(this->next_path != nullptr);
|
||||
if (IsNullTerminator(this->next_path[0]) && IsSeparator(this->next_path[-1])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsCurrentDirectory(this->next_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsParentDirectory(this->next_path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Result PathParser::GetAsDirectoryName(RomEntryName *out) const {
|
||||
AMS_ABORT_UNLESS(out != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_start != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_end != nullptr);
|
||||
AMS_ABORT_UNLESS(this->next_path != nullptr);
|
||||
|
||||
const size_t len = this->prev_path_end - this->prev_path_start;
|
||||
R_UNLESS(len <= MaxPathLength, fs::ResultDbmDirectoryNameTooLong());
|
||||
|
||||
out->length = len;
|
||||
out->path = this->prev_path_start;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result PathParser::GetAsFileName(RomEntryName *out) const {
|
||||
AMS_ABORT_UNLESS(out != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_start != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_end != nullptr);
|
||||
AMS_ABORT_UNLESS(this->next_path != nullptr);
|
||||
|
||||
const size_t len = this->prev_path_end - this->prev_path_start;
|
||||
R_UNLESS(len <= MaxPathLength, fs::ResultDbmFileNameTooLong());
|
||||
|
||||
out->length = len;
|
||||
out->path = this->prev_path_start;
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result PathParser::GetNextDirectoryName(RomEntryName *out) {
|
||||
AMS_ABORT_UNLESS(out != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_start != nullptr);
|
||||
AMS_ABORT_UNLESS(this->prev_path_end != nullptr);
|
||||
AMS_ABORT_UNLESS(this->next_path != nullptr);
|
||||
|
||||
/* Set the current path to output. */
|
||||
out->length = this->prev_path_end - this->prev_path_start;
|
||||
out->path = this->prev_path_start;
|
||||
|
||||
/* Parse the next path. */
|
||||
this->prev_path_start = this->next_path;
|
||||
const RomPathChar *cur = this->next_path;
|
||||
for (size_t name_len = 0; true; name_len++) {
|
||||
if (IsSeparator(cur[name_len])) {
|
||||
R_UNLESS(name_len < MaxPathLength, fs::ResultDbmDirectoryNameTooLong());
|
||||
|
||||
this->prev_path_end = cur + name_len;
|
||||
this->next_path = this->prev_path_end + 1;
|
||||
|
||||
while (IsSeparator(this->next_path[0])) {
|
||||
this->next_path++;
|
||||
}
|
||||
if (IsNullTerminator(this->next_path[0])) {
|
||||
this->finished = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsNullTerminator(cur[name_len])) {
|
||||
this->finished = true;
|
||||
this->prev_path_end = this->next_path = cur + name_len;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
Result GetParentDirectoryName(RomEntryName *out, const RomEntryName &cur, const RomPathChar *p) {
|
||||
AMS_ABORT_UNLESS(out != nullptr);
|
||||
AMS_ABORT_UNLESS(p != nullptr);
|
||||
|
||||
const RomPathChar *start = cur.path;
|
||||
const RomPathChar *end = cur.path + cur.length - 1;
|
||||
|
||||
s32 depth = 1;
|
||||
if (IsParentDirectory(cur)) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (cur.path > p) {
|
||||
size_t len = 0;
|
||||
const RomPathChar *head = cur.path - 1;
|
||||
while (head >= p) {
|
||||
if (IsSeparator(*head)) {
|
||||
if (IsCurrentDirectory(head + 1, len)) {
|
||||
depth++;
|
||||
}
|
||||
|
||||
if (IsParentDirectory(head + 1, len)) {
|
||||
depth += 2;
|
||||
}
|
||||
|
||||
if (depth == 0) {
|
||||
start = head + 1;
|
||||
}
|
||||
|
||||
while (IsSeparator(*head)) {
|
||||
head--;
|
||||
}
|
||||
|
||||
end = head;
|
||||
len = 0;
|
||||
depth--;
|
||||
}
|
||||
|
||||
len++;
|
||||
head--;
|
||||
}
|
||||
|
||||
R_UNLESS(depth == 0, fs::ResultDirectoryUnobtainable());
|
||||
|
||||
if (head == p) {
|
||||
start = p + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (end <= p) {
|
||||
out->path = p;
|
||||
out->length = 0;
|
||||
} else {
|
||||
out->path = start;
|
||||
out->length = end - start + 1;
|
||||
}
|
||||
|
||||
return ResultSuccess();
|
||||
}
|
||||
|
||||
}
|
|
@ -140,6 +140,12 @@ namespace ams::fs {
|
|||
R_DEFINE_ERROR_RESULT(MapFull, 6811);
|
||||
|
||||
R_DEFINE_ERROR_RANGE(BadState, 6900, 6999);
|
||||
R_DEFINE_ERROR_RESULT(NotMounted, 6905);
|
||||
R_DEFINE_ERROR_RESULT(NotInitialized, 6902);
|
||||
R_DEFINE_ERROR_RESULT(NotMounted, 6905);
|
||||
|
||||
R_DEFINE_ERROR_RESULT(DbmInvalidOperation, 7914);
|
||||
R_DEFINE_ERROR_RESULT(DbmInvalidPathFormat, 7915);
|
||||
R_DEFINE_ERROR_RESULT(DbmDirectoryNameTooLong, 7916);
|
||||
R_DEFINE_ERROR_RESULT(DbmFileNameTooLong, 7917);
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue