2018-02-04 03:22:01 +01:00
|
|
|
// Matrix Construct
|
|
|
|
//
|
|
|
|
// Copyright (C) Matrix Construct Developers, Authors & Contributors
|
|
|
|
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
|
|
|
|
//
|
|
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
|
|
// copyright notice and this permission notice is present in all copies. The
|
|
|
|
// full license for this software is available in the LICENSE file.
|
2017-08-23 23:10:28 +02:00
|
|
|
|
|
|
|
#pragma once
|
|
|
|
#define HAVE_IRCD_M_ID_H
|
|
|
|
|
2017-09-07 13:07:58 +02:00
|
|
|
namespace ircd::m
|
|
|
|
{
|
2017-12-12 21:13:06 +01:00
|
|
|
struct id;
|
|
|
|
|
2017-09-07 13:07:58 +02:00
|
|
|
IRCD_M_EXCEPTION(error, INVALID_MXID, http::BAD_REQUEST)
|
|
|
|
IRCD_M_EXCEPTION(INVALID_MXID, BAD_SIGIL, http::BAD_REQUEST)
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
bool my(const id &);
|
2018-03-03 13:05:43 +01:00
|
|
|
bool is_sigil(const char &c) noexcept;
|
|
|
|
bool has_sigil(const string_view &) noexcept;
|
2017-08-23 23:10:28 +02:00
|
|
|
}
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (Appendix 4.2) Common Identifier Format
|
|
|
|
///
|
|
|
|
/// The Matrix protocol uses a common format to assign unique identifiers to
|
|
|
|
/// a number of entities, including users, events and rooms. Each identifier
|
|
|
|
/// takes the form: `&localpart:domain` where & represents a 'sigil' character;
|
|
|
|
/// domain is the server name of the homeserver which allocated the identifier,
|
|
|
|
/// and localpart is an identifier allocated by that homeserver. The precise
|
|
|
|
/// grammar defining the allowable format of an identifier depends on the type
|
|
|
|
/// of identifier.
|
|
|
|
///
|
|
|
|
/// This structure is an interface to a string representing an mxid. The m::id
|
|
|
|
/// itself is just a string_view over some existing data. m::id::buf is an
|
|
|
|
/// m::id with an internal array providing the buffer.
|
|
|
|
///
|
2017-09-07 13:07:58 +02:00
|
|
|
struct ircd::m::id
|
|
|
|
:string_view
|
2017-08-23 23:10:28 +02:00
|
|
|
{
|
2017-09-07 13:07:58 +02:00
|
|
|
struct event;
|
|
|
|
struct user;
|
|
|
|
struct room;
|
2017-12-12 21:13:06 +01:00
|
|
|
struct room_alias;
|
|
|
|
struct group;
|
2018-02-15 22:01:07 +01:00
|
|
|
struct device;
|
2017-10-16 06:48:53 +02:00
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
enum sigil :char;
|
2018-04-25 04:18:50 +02:00
|
|
|
template<class T> struct buf;
|
2018-12-21 22:48:03 +01:00
|
|
|
struct input;
|
|
|
|
struct output;
|
2017-11-16 02:48:25 +01:00
|
|
|
struct parser;
|
2017-12-12 21:13:06 +01:00
|
|
|
struct printer;
|
2019-07-09 10:59:00 +02:00
|
|
|
struct valid;
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2017-11-16 02:48:25 +01:00
|
|
|
struct parser static const parser;
|
2017-12-12 21:13:06 +01:00
|
|
|
struct printer static const printer;
|
2019-07-09 10:59:00 +02:00
|
|
|
struct valid static const valid;
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2018-05-02 01:51:35 +02:00
|
|
|
static constexpr const size_t &MAX_SIZE
|
2018-04-25 04:18:50 +02:00
|
|
|
{
|
2018-08-12 19:16:00 +02:00
|
|
|
RB_MXID_MAXLEN // set by ./configure; should be 255
|
2018-04-25 04:18:50 +02:00
|
|
|
};
|
|
|
|
|
2017-11-16 02:48:25 +01:00
|
|
|
public:
|
2017-09-07 13:07:58 +02:00
|
|
|
// Extract elements
|
2018-03-14 00:22:47 +01:00
|
|
|
string_view local() const; // The full localpart including sigil
|
|
|
|
string_view host() const; // The full server part including port
|
|
|
|
string_view localname() const; // The localpart not including sigil
|
|
|
|
string_view hostname() const; // The server part not including port
|
2018-04-04 22:03:13 +02:00
|
|
|
uint16_t port() const; // Just the port number or 0 if none
|
2019-02-26 22:24:46 +01:00
|
|
|
bool literal() const; // Whether the hostname() is IP literal
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2019-03-31 00:35:05 +01:00
|
|
|
// Rewrites the ID so the local and host parts are swapped; for indexing.
|
|
|
|
// e.g. `$foo:bar.com` becomes `bar.com$foo`; unswap() reverses to normal.
|
|
|
|
static id unswap(const string_view &, const mutable_buffer &);
|
|
|
|
static string_view swap(const id &, const mutable_buffer &);
|
|
|
|
string_view swap(const mutable_buffer &) const;
|
|
|
|
|
2017-09-07 13:07:58 +02:00
|
|
|
IRCD_USING_OVERLOAD(generate, m::generate);
|
|
|
|
|
2017-10-16 06:48:53 +02:00
|
|
|
id(const enum sigil &, const mutable_buffer &, const generate_t &, const string_view &host);
|
2018-01-19 05:44:55 +01:00
|
|
|
id(const enum sigil &, const mutable_buffer &, const string_view &local, const string_view &host);
|
2017-10-16 06:48:53 +02:00
|
|
|
id(const enum sigil &, const mutable_buffer &, const string_view &id);
|
2017-10-03 13:07:10 +02:00
|
|
|
id(const enum sigil &, const string_view &id);
|
2020-11-30 02:48:10 +01:00
|
|
|
explicit id(const enum sigil &, const id &);
|
2017-10-03 13:07:10 +02:00
|
|
|
id(const string_view &id);
|
|
|
|
id() = default;
|
2017-09-07 13:07:58 +02:00
|
|
|
};
|
|
|
|
|
2019-02-05 12:09:40 +01:00
|
|
|
// Utilities
|
|
|
|
namespace ircd::m
|
|
|
|
{
|
|
|
|
// Check if any sigil
|
|
|
|
bool is_sigil(const char &c) noexcept;
|
|
|
|
bool has_sigil(const string_view &) noexcept;
|
|
|
|
|
|
|
|
// Interpret sigil type (or throw)
|
|
|
|
id::sigil sigil(const char &c);
|
|
|
|
id::sigil sigil(const string_view &id);
|
|
|
|
string_view reflect(const id::sigil &);
|
|
|
|
|
|
|
|
// Full ID checks
|
2019-07-09 10:59:00 +02:00
|
|
|
void validate(const id::sigil &, const string_view &); // valid() | throws
|
2019-02-05 12:09:40 +01:00
|
|
|
bool valid(const id::sigil &, const string_view &) noexcept;
|
|
|
|
bool valid_local(const id::sigil &, const string_view &) noexcept; // Local part is valid
|
|
|
|
bool valid_local_only(const id::sigil &, const string_view &) noexcept; // No :host
|
|
|
|
}
|
|
|
|
|
2019-07-09 10:59:00 +02:00
|
|
|
/// Singleton in m::id
|
|
|
|
struct ircd::m::id::valid
|
|
|
|
{
|
|
|
|
bool operator()(std::nothrow_t, const id::sigil &sigil, const string_view &id) const noexcept;
|
|
|
|
void operator()(const id::sigil &sigil, const string_view &id) const;
|
|
|
|
bool operator()(std::nothrow_t, const string_view &id) const noexcept;
|
|
|
|
void operator()(const string_view &id) const;
|
|
|
|
};
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (4.2) The sigil characters
|
2017-11-16 02:48:25 +01:00
|
|
|
enum ircd::m::id::sigil
|
|
|
|
:char
|
|
|
|
{
|
2017-12-12 21:13:06 +01:00
|
|
|
USER = '@', ///< User ID (4.2.1)
|
|
|
|
EVENT = '$', ///< Event ID (4.2.2)
|
|
|
|
ROOM = '!', ///< Room ID (4.2.2)
|
|
|
|
ROOM_ALIAS = '#', ///< Room alias (4.2.3)
|
|
|
|
GROUP = '+', ///< Group ID (experimental)
|
2018-02-15 22:01:07 +01:00
|
|
|
DEVICE = '%', ///< Device ID (experimental)
|
2019-08-12 07:51:26 +02:00
|
|
|
NODE = ':', ///< Node ID (experimental)
|
2017-11-16 02:48:25 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (Appendix 4.2.1) User Identifiers
|
|
|
|
///
|
|
|
|
/// Users within Matrix are uniquely identified by their Matrix user ID. The
|
|
|
|
/// user ID is namespaced to the homeserver which allocated the account and
|
|
|
|
/// has the form: `@localpart:domain` The localpart of a user ID is an opaque
|
|
|
|
/// identifier for that user. It MUST NOT be empty, and MUST contain only the
|
|
|
|
/// characters a-z, 0-9, ., _, =, -, and /. The domain of a user ID is the
|
|
|
|
/// server name of the homeserver which allocated the account. The length of
|
|
|
|
/// a user ID, including the @ sigil and the domain, MUST NOT exceed 255
|
|
|
|
/// characters.
|
|
|
|
///
|
|
|
|
struct ircd::m::id::user
|
|
|
|
:ircd::m::id
|
2017-08-23 23:10:28 +02:00
|
|
|
{
|
2017-12-12 21:13:06 +01:00
|
|
|
using buf = m::id::buf<user>;
|
2018-10-01 02:16:06 +02:00
|
|
|
|
|
|
|
template<class... args>
|
|
|
|
user(args&&... a)
|
|
|
|
:m::id{USER, std::forward<args>(a)...}
|
|
|
|
{}
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
user() = default;
|
|
|
|
};
|
2017-10-28 21:26:47 +02:00
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (Appendix 4.2.2) Room IDs and Event IDs
|
|
|
|
///
|
|
|
|
/// An event has exactly one event ID. An event ID has the format:
|
|
|
|
/// `$opaque_id:domain` The domain of an event ID is the server name of the
|
|
|
|
/// homeserver which created the event. The domain is used only for namespacing
|
|
|
|
/// to avoid the risk of clashes of identifiers between different homeservers.
|
|
|
|
/// There is no implication that the event in question is still available at
|
|
|
|
/// the corresponding homeserver. Event IDs are case-sensitive. They are not
|
|
|
|
/// meant to be human readable.
|
|
|
|
///
|
2017-10-28 21:26:47 +02:00
|
|
|
struct ircd::m::id::event
|
|
|
|
:ircd::m::id
|
|
|
|
{
|
2019-06-26 13:15:56 +02:00
|
|
|
struct v3;
|
|
|
|
struct v4;
|
|
|
|
|
2018-10-01 02:16:06 +02:00
|
|
|
using buf = m::id::buf<event>;
|
2018-02-10 07:13:18 +01:00
|
|
|
using closure = std::function<void (const id::event &)>;
|
|
|
|
using closure_bool = std::function<bool (const id::event &)>;
|
|
|
|
|
2019-06-26 12:35:34 +02:00
|
|
|
string_view version() const; // static view
|
|
|
|
|
2018-10-01 02:16:06 +02:00
|
|
|
template<class... args>
|
|
|
|
event(args&&... a)
|
|
|
|
:m::id{EVENT, std::forward<args>(a)...}
|
|
|
|
{}
|
|
|
|
|
2017-11-16 02:48:25 +01:00
|
|
|
event() = default;
|
2017-10-28 21:26:47 +02:00
|
|
|
};
|
|
|
|
|
2019-07-03 22:00:25 +02:00
|
|
|
// Forward declaration for m::id::event::v3/v4 ctors which hash an m::event
|
|
|
|
// to compute its event_id. The m::id::event::v3/v4 objects could be placed
|
|
|
|
// elsewhere below event.h to avoid circularity, but it is probably more
|
|
|
|
// intuitive for developers to to find these objects filed here instead.
|
|
|
|
namespace ircd::m
|
|
|
|
{
|
|
|
|
struct event;
|
|
|
|
}
|
|
|
|
|
2019-06-26 13:15:56 +02:00
|
|
|
/// Version 3 event_id
|
2019-07-03 22:00:25 +02:00
|
|
|
///
|
|
|
|
/// This object is constructed on either a fully qualified existing event_id
|
|
|
|
/// to which it validates and maintains a string_view similar to other m::id
|
|
|
|
/// constructions, or the event data itself from which it creates a version 3
|
|
|
|
/// event_id into the buffer to maintain a string_view like the former.
|
2019-06-26 13:15:56 +02:00
|
|
|
struct ircd::m::id::event::v3
|
|
|
|
:ircd::m::id::event
|
|
|
|
{
|
2019-06-28 07:56:50 +02:00
|
|
|
static bool is(const string_view &) noexcept;
|
|
|
|
|
2019-06-28 04:46:37 +02:00
|
|
|
v3(const mutable_buffer &out, const m::event &);
|
2019-06-26 13:15:56 +02:00
|
|
|
v3(const string_view &id);
|
|
|
|
v3() = default;
|
|
|
|
};
|
|
|
|
|
|
|
|
/// Version 4 event_id
|
2019-07-03 22:00:25 +02:00
|
|
|
///
|
|
|
|
/// The version 4 event_id is similar to the version 3 event_id except for
|
|
|
|
/// being RFC4648 (base64 URL-safe) encoded. The v3 event_id is deprecated
|
|
|
|
/// by Matrix-spec. See v3 notes.
|
2019-06-26 13:15:56 +02:00
|
|
|
struct ircd::m::id::event::v4
|
|
|
|
:ircd::m::id::event
|
|
|
|
{
|
2019-06-28 07:56:50 +02:00
|
|
|
static bool is(const string_view &) noexcept;
|
|
|
|
|
2019-06-28 04:46:37 +02:00
|
|
|
v4(const mutable_buffer &out, const m::event &);
|
2019-06-26 13:15:56 +02:00
|
|
|
v4(const string_view &id);
|
|
|
|
v4() = default;
|
|
|
|
};
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (Appendix 4.2.2) Room IDs and Event IDs
|
|
|
|
///
|
|
|
|
/// A room has exactly one room ID. A room ID has the format:
|
|
|
|
/// `!opaque_id:domain` The domain of a room ID is the server name of the
|
|
|
|
/// homeserver which created the room. The domain is used only for namespacing
|
|
|
|
/// to avoid the risk of clashes of identifiers between different homeservers.
|
|
|
|
/// There is no implication that the room in question is still available at
|
|
|
|
/// the corresponding homeserver. Room IDs are case-sensitive. They are not
|
|
|
|
/// meant to be human readable.
|
|
|
|
///
|
2017-10-28 21:26:47 +02:00
|
|
|
struct ircd::m::id::room
|
|
|
|
:ircd::m::id
|
|
|
|
{
|
2018-10-01 02:16:06 +02:00
|
|
|
using buf = m::id::buf<room>;
|
2018-02-10 07:13:18 +01:00
|
|
|
using closure = std::function<void (const id::room &)>;
|
|
|
|
using closure_bool = std::function<bool (const id::room &)>;
|
|
|
|
|
2018-10-01 02:16:06 +02:00
|
|
|
template<class... args>
|
|
|
|
room(args&&... a)
|
|
|
|
:m::id{ROOM, std::forward<args>(a)...}
|
|
|
|
{}
|
|
|
|
|
2017-11-16 02:48:25 +01:00
|
|
|
room() = default;
|
2017-10-28 21:26:47 +02:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
/// (Appendix 4.2.3) Room Aliases
|
|
|
|
/// A room may have zero or more aliases. A room alias has the format:
|
|
|
|
/// `#room_alias:domain` The domain of a room alias is the server name of the
|
|
|
|
/// homeserver which created the alias. Other servers may contact this
|
|
|
|
/// homeserver to look up the alias. Room aliases MUST NOT exceed 255 bytes
|
|
|
|
/// (including the # sigil and the domain).
|
|
|
|
///
|
|
|
|
struct ircd::m::id::room_alias
|
|
|
|
:ircd::m::id
|
|
|
|
{
|
|
|
|
using buf = m::id::buf<room_alias>;
|
2018-10-01 02:16:06 +02:00
|
|
|
|
|
|
|
template<class... args>
|
|
|
|
room_alias(args&&... a)
|
|
|
|
:m::id{ROOM_ALIAS, std::forward<args>(a)...}
|
|
|
|
{}
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
room_alias() = default;
|
|
|
|
};
|
|
|
|
|
|
|
|
/// Group ID (EXPERIMENTAL)
|
|
|
|
///
|
|
|
|
struct ircd::m::id::group
|
2017-10-28 21:26:47 +02:00
|
|
|
:ircd::m::id
|
|
|
|
{
|
2017-12-12 21:13:06 +01:00
|
|
|
using buf = m::id::buf<group>;
|
2018-10-01 02:16:06 +02:00
|
|
|
|
|
|
|
template<class... args>
|
|
|
|
group(args&&... a)
|
|
|
|
:m::id{GROUP, std::forward<args>(a)...}
|
|
|
|
{}
|
|
|
|
|
2017-12-12 21:13:06 +01:00
|
|
|
group() = default;
|
2017-10-28 21:26:47 +02:00
|
|
|
};
|
|
|
|
|
2018-10-01 02:47:41 +02:00
|
|
|
/// Device ID (EXPERIMENTAL)
|
2017-12-12 21:13:06 +01:00
|
|
|
///
|
2018-10-01 02:47:41 +02:00
|
|
|
struct ircd::m::id::device
|
2017-12-12 21:13:06 +01:00
|
|
|
:ircd::m::id
|
|
|
|
{
|
2018-10-01 02:47:41 +02:00
|
|
|
using buf = m::id::buf<device>;
|
2018-10-01 02:16:06 +02:00
|
|
|
|
|
|
|
template<class... args>
|
2018-10-01 02:47:41 +02:00
|
|
|
device(args&&... a)
|
|
|
|
:m::id{DEVICE, std::forward<args>(a)...}
|
2018-10-01 02:16:06 +02:00
|
|
|
{}
|
|
|
|
|
2018-10-01 02:47:41 +02:00
|
|
|
device() = default;
|
2017-12-12 21:13:06 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/// ID object backed by an internal buffer of default worst-case size.
|
2017-10-25 18:47:03 +02:00
|
|
|
///
|
2018-04-25 04:18:50 +02:00
|
|
|
template<class T>
|
2017-09-07 13:07:58 +02:00
|
|
|
struct ircd::m::id::buf
|
|
|
|
:T
|
2017-08-23 23:10:28 +02:00
|
|
|
{
|
2017-09-25 03:05:42 +02:00
|
|
|
static constexpr const size_t SIZE
|
|
|
|
{
|
2019-08-03 06:15:57 +02:00
|
|
|
m::id::MAX_SIZE + 1
|
2017-09-25 03:05:42 +02:00
|
|
|
};
|
2017-09-07 13:07:58 +02:00
|
|
|
|
|
|
|
private:
|
2019-08-03 06:15:57 +02:00
|
|
|
fixed_buffer<mutable_buffer, SIZE> b;
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2017-09-07 13:07:58 +02:00
|
|
|
public:
|
2019-08-03 06:15:57 +02:00
|
|
|
operator const fixed_buffer<mutable_buffer, SIZE> &() const
|
2017-10-01 09:16:05 +02:00
|
|
|
{
|
|
|
|
return b;
|
|
|
|
}
|
|
|
|
|
|
|
|
operator mutable_buffer()
|
|
|
|
{
|
|
|
|
return b;
|
|
|
|
}
|
|
|
|
|
2018-03-05 09:52:24 +01:00
|
|
|
/// Due to the normal semantics of this object in relation to its parent,
|
|
|
|
/// if you write directly to this as a mutable_buffer you can call
|
|
|
|
/// assigned() to update this.
|
|
|
|
buf &assigned(const T &t)
|
|
|
|
{
|
|
|
|
assert(string_view{t}.data() == b.data());
|
|
|
|
static_cast<string_view &>(*this) = t;
|
|
|
|
return *this;
|
|
|
|
}
|
|
|
|
|
2018-05-14 03:43:51 +02:00
|
|
|
// Universal reference with perfect forwarding will be too greedy here by
|
|
|
|
// overriding some of the other semantics. const reference pack is fine.
|
2017-09-07 13:07:58 +02:00
|
|
|
template<class... args>
|
2018-05-14 03:43:51 +02:00
|
|
|
buf(const args &...a)
|
|
|
|
:T{b, a...}
|
2017-08-23 23:10:28 +02:00
|
|
|
{}
|
|
|
|
|
2017-09-07 13:07:58 +02:00
|
|
|
buf() = default;
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2017-10-28 21:26:47 +02:00
|
|
|
buf(const buf &other)
|
2018-03-16 23:06:53 +01:00
|
|
|
:T{}
|
|
|
|
{
|
|
|
|
static_cast<string_view &>(*this) =
|
|
|
|
{
|
|
|
|
b.data(), buffer::copy(b, string_view{other})
|
|
|
|
};
|
|
|
|
}
|
2017-08-23 23:10:28 +02:00
|
|
|
|
2018-03-16 23:06:53 +01:00
|
|
|
buf(buf &&other) noexcept
|
|
|
|
:T{}
|
2017-10-28 21:26:47 +02:00
|
|
|
{
|
2018-03-16 23:06:53 +01:00
|
|
|
static_cast<string_view &>(*this) =
|
|
|
|
{
|
|
|
|
b.data(), buffer::copy(b, string_view{other})
|
|
|
|
};
|
2017-10-28 21:26:47 +02:00
|
|
|
}
|
2017-09-07 13:07:58 +02:00
|
|
|
|
2018-03-16 23:06:53 +01:00
|
|
|
buf &operator=(const buf &other)
|
2017-10-28 21:26:47 +02:00
|
|
|
{
|
2018-03-16 23:06:53 +01:00
|
|
|
this->~buf();
|
|
|
|
static_cast<string_view &>(*this) =
|
|
|
|
{
|
|
|
|
b.data(), buffer::copy(b, string_view{other})
|
|
|
|
};
|
|
|
|
|
2017-10-28 21:26:47 +02:00
|
|
|
return *this;
|
|
|
|
}
|
2017-09-07 13:07:58 +02:00
|
|
|
};
|
2019-10-10 07:15:24 +02:00
|
|
|
|
|
|
|
inline
|
|
|
|
ircd::m::id::id(const string_view &str)
|
|
|
|
:id
|
|
|
|
{
|
|
|
|
m::sigil(str), str
|
|
|
|
}
|
2020-11-30 02:48:10 +01:00
|
|
|
{}
|
|
|
|
|
|
|
|
inline
|
|
|
|
ircd::m::id::id(const id::sigil &sigil,
|
|
|
|
const id &id)
|
|
|
|
:string_view{id}
|
2019-10-10 07:15:24 +02:00
|
|
|
{
|
2020-11-30 02:48:10 +01:00
|
|
|
assert(this->empty() || this->front() == sigil);
|
2019-10-10 07:15:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
inline
|
|
|
|
ircd::m::id::id(const id::sigil &sigil,
|
|
|
|
const string_view &id)
|
|
|
|
:string_view{id}
|
|
|
|
{
|
|
|
|
valid(sigil, id);
|
|
|
|
}
|