Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
Actor Framework
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
cpp-libs
Actor Framework
Commits
2aeb1e8b
Commit
2aeb1e8b
authored
Oct 29, 2022
by
Dominik Charousset
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'topic/neverlord/caf-net-frames'
parents
063022f1
6ada45be
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
377 additions
and
56 deletions
+377
-56
libcaf_net/CMakeLists.txt
libcaf_net/CMakeLists.txt
+2
-0
libcaf_net/caf/net/binary/frame.hpp
libcaf_net/caf/net/binary/frame.hpp
+25
-2
libcaf_net/caf/net/web_socket/frame.hpp
libcaf_net/caf/net/web_socket/frame.hpp
+78
-39
libcaf_net/src/net/binary/frame.cpp
libcaf_net/src/net/binary/frame.cpp
+25
-5
libcaf_net/src/net/web_socket/frame.cpp
libcaf_net/src/net/web_socket/frame.cpp
+63
-10
libcaf_net/test/net/binary/frame.cpp
libcaf_net/test/net/binary/frame.cpp
+77
-0
libcaf_net/test/net/web_socket/frame.cpp
libcaf_net/test/net/web_socket/frame.cpp
+107
-0
No files found.
libcaf_net/CMakeLists.txt
View file @
2aeb1e8b
...
...
@@ -94,6 +94,7 @@ caf_add_component(
detail.rfc6455
net.accept_socket
net.actor_shell
net.binary.frame
net.datagram_socket
net.http.server
net.ip
...
...
@@ -112,5 +113,6 @@ caf_add_component(
net.typed_actor_shell
net.udp_datagram_socket
net.web_socket.client
net.web_socket.frame
net.web_socket.handshake
net.web_socket.server
)
libcaf_net/caf/net/binary/frame.hpp
View file @
2aeb1e8b
...
...
@@ -19,13 +19,30 @@ namespace caf::net::binary {
/// An implicitly shared type for binary data frames.
class
CAF_NET_EXPORT
frame
{
public:
// -- constructors, destructors, and assignment operators --------------------
frame
()
=
default
;
frame
(
frame
&&
)
=
default
;
frame
(
const
frame
&
)
=
default
;
frame
&
operator
=
(
frame
&&
)
=
default
;
frame
&
operator
=
(
const
frame
&
)
=
default
;
explicit
frame
(
const_byte_span
data
);
explicit
frame
(
const_byte_span
buf
);
// -- factory functions ------------------------------------------------------
template
<
class
...
ByteBuffers
>
static
frame
from_buffers
(
const
ByteBuffers
&
...
buffers
)
{
static_assert
(
sizeof
...(
ByteBuffers
)
>
0
);
const_byte_span
bufs
[
sizeof
...(
ByteBuffers
)]
=
{
make_span
(
buffers
)...};
return
frame
(
make_span
(
bufs
));
}
// -- properties -------------------------------------------------------------
explicit
operator
bool
()
const
noexcept
{
return
static_cast
<
bool
>
(
data_
);
...
...
@@ -44,10 +61,16 @@ public:
}
const_byte_span
bytes
()
const
noexcept
{
return
{
data_
->
storage
(),
data_
->
size
()};
return
data_
?
const_byte_span
{
data_
->
storage
(),
data_
->
size
()}
:
const_byte_span
{};
}
private:
explicit
frame
(
caf
::
span
<
const
const_byte_span
>
bufs
);
template
<
class
...
Args
>
void
init
(
size_t
payload_size
,
Args
&&
...
arg
);
intrusive_ptr
<
web_socket
::
frame
::
data
>
data_
;
};
...
...
libcaf_net/caf/net/web_socket/frame.hpp
View file @
2aeb1e8b
...
...
@@ -34,43 +34,7 @@ namespace caf::net::web_socket {
/// text or binary frames.
class
CAF_NET_EXPORT
frame
{
public:
frame
()
=
default
;
frame
(
frame
&&
)
=
default
;
frame
(
const
frame
&
)
=
default
;
frame
&
operator
=
(
frame
&&
)
=
default
;
frame
&
operator
=
(
const
frame
&
)
=
default
;
explicit
frame
(
const_byte_span
bytes
);
explicit
frame
(
std
::
string_view
text
);
explicit
operator
bool
()
const
noexcept
{
return
static_cast
<
bool
>
(
data_
);
}
size_t
size
()
const
noexcept
{
return
data_
?
data_
->
size
()
:
0
;
}
bool
empty
()
const
noexcept
{
return
data_
?
data_
->
size
()
==
0
:
true
;
}
void
swap
(
frame
&
other
)
{
data_
.
swap
(
other
.
data_
);
}
bool
is_binary
()
const
noexcept
{
return
data_
&&
data_
->
is_binary
();
}
bool
is_text
()
const
noexcept
{
return
data_
&&
!
data_
->
is_binary
();
}
const_byte_span
as_binary
()
const
noexcept
;
std
::
string_view
as_text
()
const
noexcept
;
// -- member types -----------------------------------------------------------
class
data
{
public:
...
...
@@ -80,10 +44,18 @@ public:
data
&
operator
=
(
const
data
&
)
=
delete
;
explicit
data
(
bool
bin
,
size_t
size
)
:
rc_
(
1
),
bin_
(
bin
),
size_
(
size
)
{
data
(
bool
is_bin
,
size_t
size
)
:
rc_
(
1
),
bin_
(
is_
bin
),
size_
(
size
)
{
static_cast
<
void
>
(
padding_
);
// Silence unused-private-field warning.
}
explicit
data
(
const_byte_span
bytes
);
explicit
data
(
std
::
string_view
str
);
data
(
size_t
total_size
,
span
<
const
const_byte_span
>
bufs
);
data
(
size_t
total_size
,
span
<
const
std
::
string_view
>
bufs
);
// -- reference counting ---------------------------------------------------
bool
unique
()
const
noexcept
{
...
...
@@ -132,12 +104,79 @@ public:
std
::
byte
storage_
[];
};
// -- constructors, destructors, and assignment operators --------------------
frame
()
=
default
;
frame
(
frame
&&
)
=
default
;
frame
(
const
frame
&
)
=
default
;
frame
&
operator
=
(
frame
&&
)
=
default
;
frame
&
operator
=
(
const
frame
&
)
=
default
;
explicit
frame
(
const_byte_span
bytes
);
explicit
frame
(
std
::
string_view
text
);
// -- factory functions ------------------------------------------------------
template
<
class
...
Buffers
>
static
frame
from_buffers
(
const
Buffers
&
...
buffers
)
{
static_assert
(
sizeof
...(
Buffers
)
>
0
);
const_byte_span
bufs
[
sizeof
...(
Buffers
)]
=
{
make_span
(
buffers
)...};
return
frame
(
make_span
(
bufs
));
}
template
<
class
...
Buffers
>
static
frame
from_strings
(
const
Buffers
&
...
buffers
)
{
static_assert
(
sizeof
...(
Buffers
)
>
0
);
std
::
string_view
bufs
[
sizeof
...(
Buffers
)]
=
{
std
::
string_view
(
buffers
)...};
return
frame
(
make_span
(
bufs
));
}
// -- properties -------------------------------------------------------------
explicit
operator
bool
()
const
noexcept
{
return
static_cast
<
bool
>
(
data_
);
}
size_t
size
()
const
noexcept
{
return
data_
?
data_
->
size
()
:
0
;
}
bool
empty
()
const
noexcept
{
return
data_
?
data_
->
size
()
==
0
:
true
;
}
void
swap
(
frame
&
other
)
{
data_
.
swap
(
other
.
data_
);
}
bool
is_binary
()
const
noexcept
{
return
data_
&&
data_
->
is_binary
();
}
bool
is_text
()
const
noexcept
{
return
data_
&&
!
data_
->
is_binary
();
}
const_byte_span
as_binary
()
const
noexcept
;
std
::
string_view
as_text
()
const
noexcept
;
private:
explicit
frame
(
intrusive_ptr
<
data
>
ptr
)
:
data_
(
std
::
move
(
ptr
))
{
// nop
}
void
alloc
(
bool
is_binary
,
size_t
num_bytes
);
explicit
frame
(
caf
::
span
<
const
const_byte_span
>
buffers
);
explicit
frame
(
caf
::
span
<
const
std
::
string_view
>
buffers
);
template
<
class
...
Args
>
void
init
(
size_t
payload_size
,
Args
&&
...
arg
);
intrusive_ptr
<
data
>
data_
;
};
...
...
libcaf_net/src/net/binary/frame.cpp
View file @
2aeb1e8b
...
...
@@ -6,14 +6,34 @@
#include <cstring>
#include <new>
#include <numeric>
namespace
caf
::
net
::
binary
{
frame
::
frame
(
const_byte_span
data
)
{
auto
total_size
=
sizeof
(
web_socket
::
frame
::
data
)
+
data
.
size
();
auto
vptr
=
malloc
(
total_size
);
data_
.
reset
(
new
(
vptr
)
web_socket
::
frame
::
data
(
true
,
data
.
size
()),
false
);
memcpy
(
data_
->
storage
(),
data
.
data
(),
data
.
size
());
namespace
{
template
<
class
Buffer
>
size_t
calc_total_size
(
span
<
const
Buffer
>
buffers
)
{
auto
fn
=
[](
size_t
n
,
const
Buffer
&
buf
)
{
return
n
+
buf
.
size
();
};
return
std
::
accumulate
(
buffers
.
begin
(),
buffers
.
end
(),
size_t
{
0
},
fn
);
}
}
// namespace
frame
::
frame
(
const_byte_span
buf
)
{
init
(
buf
.
size
(),
buf
);
}
frame
::
frame
(
caf
::
span
<
const
const_byte_span
>
bufs
)
{
auto
payload_size
=
calc_total_size
(
bufs
);
init
(
payload_size
,
payload_size
,
bufs
);
}
template
<
class
...
Args
>
void
frame
::
init
(
size_t
payload_size
,
Args
&&
...
args
)
{
using
data_t
=
web_socket
::
frame
::
data
;
auto
vptr
=
malloc
(
sizeof
(
data_t
)
+
payload_size
);
data_
.
reset
(
new
(
vptr
)
data_t
(
std
::
forward
<
Args
>
(
args
)...),
false
);
}
}
// namespace caf::net::binary
libcaf_net/src/net/web_socket/frame.cpp
View file @
2aeb1e8b
...
...
@@ -6,25 +6,78 @@
#include <cstring>
#include <new>
#include <numeric>
namespace
caf
::
net
::
web_socket
{
namespace
{
template
<
class
Buffer
>
size_t
calc_total_size
(
span
<
const
Buffer
>
buffers
)
{
auto
fn
=
[](
size_t
n
,
const
Buffer
&
buf
)
{
return
n
+
buf
.
size
();
};
return
std
::
accumulate
(
buffers
.
begin
(),
buffers
.
end
(),
size_t
{
0
},
fn
);
}
}
// namespace
frame
::
data
::
data
(
const_byte_span
bytes
)
:
data
(
true
,
bytes
.
size
())
{
memcpy
(
storage
(),
bytes
.
data
(),
bytes
.
size
());
}
frame
::
data
::
data
(
std
::
string_view
str
)
:
data
(
false
,
str
.
size
())
{
memcpy
(
storage
(),
str
.
data
(),
str
.
size
());
}
frame
::
data
::
data
(
size_t
total_size
,
span
<
const
const_byte_span
>
bufs
)
:
data
(
true
,
total_size
)
{
std
::
byte
*
pos
=
storage_
;
for
(
const
auto
&
buf
:
bufs
)
{
if
(
!
buf
.
empty
())
{
memcpy
(
pos
,
buf
.
data
(),
buf
.
size
());
pos
+=
buf
.
size
();
}
}
}
frame
::
data
::
data
(
size_t
total_size
,
span
<
const
std
::
string_view
>
bufs
)
:
data
(
false
,
total_size
)
{
std
::
byte
*
pos
=
storage_
;
for
(
const
auto
&
buf
:
bufs
)
{
if
(
!
buf
.
empty
())
{
memcpy
(
pos
,
buf
.
data
(),
buf
.
size
());
pos
+=
buf
.
size
();
}
}
}
frame
::
frame
(
const_byte_span
bytes
)
{
alloc
(
true
,
bytes
.
size
());
memcpy
(
data_
->
storage
(),
bytes
.
data
(),
bytes
.
size
());
init
(
bytes
.
size
(),
bytes
);
}
frame
::
frame
(
std
::
string_view
text
)
{
alloc
(
false
,
text
.
size
());
memcpy
(
data_
->
storage
(),
text
.
data
(),
text
.
size
());
init
(
text
.
size
(),
text
);
}
frame
::
frame
(
caf
::
span
<
const
const_byte_span
>
buffers
)
{
auto
payload_size
=
calc_total_size
(
buffers
);
init
(
payload_size
,
payload_size
,
buffers
);
}
frame
::
frame
(
caf
::
span
<
const
std
::
string_view
>
buffers
)
{
auto
payload_size
=
calc_total_size
(
buffers
);
init
(
payload_size
,
payload_size
,
buffers
);
}
const_byte_span
frame
::
as_binary
()
const
noexcept
{
return
{
data_
->
storage
(),
data_
->
size
()};
using
res_t
=
const_byte_span
;
return
data_
?
res_t
{
data_
->
storage
(),
data_
->
size
()}
:
res_t
{};
}
std
::
string_view
frame
::
as_text
()
const
noexcept
{
return
{
reinterpret_cast
<
const
char
*>
(
data_
->
storage
()),
data_
->
size
()};
using
res_t
=
std
::
string_view
;
return
data_
?
res_t
{
reinterpret_cast
<
const
char
*>
(
data_
->
storage
()),
data_
->
size
()}
:
res_t
{};
}
void
frame
::
data
::
deref
()
noexcept
{
...
...
@@ -34,10 +87,10 @@ void frame::data::deref() noexcept {
}
}
void
frame
::
alloc
(
bool
is_binary
,
size_t
num_bytes
)
{
auto
total_size
=
sizeof
(
data
)
+
num_bytes
;
auto
vptr
=
malloc
(
total
_size
);
data_
.
reset
(
new
(
vptr
)
data
(
is_binary
,
num_bytes
),
false
);
template
<
class
...
Args
>
void
frame
::
init
(
size_t
payload_size
,
Args
&&
...
args
)
{
auto
vptr
=
malloc
(
sizeof
(
data
)
+
payload
_size
);
data_
.
reset
(
new
(
vptr
)
data
(
std
::
forward
<
Args
>
(
args
)...
),
false
);
}
}
// namespace caf::net::web_socket
libcaf_net/test/net/binary/frame.cpp
0 → 100644
View file @
2aeb1e8b
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE net.binary.frame
#include "caf/net/binary/frame.hpp"
#include "net-test.hpp"
using
namespace
caf
;
namespace
{
template
<
class
...
Ts
>
std
::
vector
<
std
::
byte
>
to_byte_buf
(
Ts
...
values
)
{
return
std
::
vector
<
std
::
byte
>
{
static_cast
<
std
::
byte
>
(
values
)...};
}
template
<
class
T
>
std
::
vector
<
std
::
byte
>
to_vec
(
span
<
const
T
>
values
)
{
return
std
::
vector
<
std
::
byte
>
{
values
.
begin
(),
values
.
end
()};
}
}
// namespace
TEST_CASE
(
"default construction"
)
{
net
::
binary
::
frame
uut
;
CHECK
(
!
uut
);
CHECK
(
uut
.
empty
());
CHECK
(
uut
.
bytes
().
empty
());
CHECK_EQ
(
uut
.
size
(),
0u
);
}
TEST_CASE
(
"construction from a single buffer"
)
{
auto
buf
=
to_byte_buf
(
1
,
2
,
3
);
auto
uut
=
net
::
binary
::
frame
{
make_span
(
buf
)};
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK
(
!
uut
.
bytes
().
empty
());
CHECK_EQ
(
uut
.
size
(),
3u
);
CHECK_EQ
(
uut
.
bytes
().
size
(),
3u
);
CHECK_EQ
(
to_vec
(
uut
.
bytes
()),
buf
);
}
TEST_CASE
(
"construction from multiple buffers"
)
{
auto
buf1
=
to_byte_buf
(
1
,
2
);
auto
buf2
=
to_byte_buf
();
auto
buf3
=
to_byte_buf
(
3
,
4
,
5
);
auto
buf4
=
to_byte_buf
(
1
,
2
,
3
,
4
,
5
);
auto
uut
=
net
::
binary
::
frame
::
from_buffers
(
buf1
,
buf2
,
buf3
);
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK
(
!
uut
.
bytes
().
empty
());
CHECK_EQ
(
uut
.
size
(),
5u
);
CHECK_EQ
(
uut
.
bytes
().
size
(),
5u
);
CHECK_EQ
(
to_vec
(
uut
.
bytes
()),
buf4
);
}
TEST_CASE
(
"copying, moving and swapping"
)
{
auto
buf
=
to_byte_buf
(
1
,
2
,
3
);
auto
uut1
=
net
::
binary
::
frame
{};
auto
uut2
=
net
::
binary
::
frame
{
make_span
(
buf
)};
auto
uut3
=
uut1
;
auto
uut4
=
uut2
;
CHECK_EQ
(
uut1
.
bytes
().
data
(),
uut3
.
bytes
().
data
());
CHECK_EQ
(
uut2
.
bytes
().
data
(),
uut4
.
bytes
().
data
());
CHECK_NE
(
uut1
.
bytes
().
data
(),
uut2
.
bytes
().
data
());
auto
uut5
=
std
::
move
(
uut1
);
auto
uut6
=
std
::
move
(
uut2
);
CHECK_EQ
(
uut5
.
bytes
().
data
(),
uut3
.
bytes
().
data
());
CHECK_EQ
(
uut6
.
bytes
().
data
(),
uut4
.
bytes
().
data
());
CHECK_NE
(
uut5
.
bytes
().
data
(),
uut6
.
bytes
().
data
());
uut5
.
swap
(
uut6
);
CHECK_EQ
(
uut6
.
bytes
().
data
(),
uut3
.
bytes
().
data
());
CHECK_EQ
(
uut5
.
bytes
().
data
(),
uut4
.
bytes
().
data
());
}
libcaf_net/test/net/web_socket/frame.cpp
0 → 100644
View file @
2aeb1e8b
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE net.web_socket.frame
#include "caf/net/web_socket/frame.hpp"
#include "net-test.hpp"
using
namespace
caf
;
using
namespace
std
::
literals
;
namespace
{
template
<
class
...
Ts
>
std
::
vector
<
std
::
byte
>
to_byte_buf
(
Ts
...
values
)
{
return
std
::
vector
<
std
::
byte
>
{
static_cast
<
std
::
byte
>
(
values
)...};
}
template
<
class
T
>
std
::
vector
<
std
::
byte
>
to_vec
(
span
<
const
T
>
values
)
{
return
std
::
vector
<
std
::
byte
>
{
values
.
begin
(),
values
.
end
()};
}
}
// namespace
TEST_CASE
(
"default construction"
)
{
net
::
web_socket
::
frame
uut
;
CHECK
(
!
uut
);
CHECK
(
!
uut
.
is_binary
());
CHECK
(
!
uut
.
is_text
());
CHECK
(
uut
.
empty
());
CHECK_EQ
(
uut
.
size
(),
0u
);
}
TEST_CASE
(
"construction from a single byte buffer"
)
{
auto
buf
=
to_byte_buf
(
1
,
2
,
3
);
auto
uut
=
net
::
web_socket
::
frame
{
make_span
(
buf
)};
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK_EQ
(
uut
.
size
(),
3u
);
if
(
uut
.
is_binary
())
{
auto
bytes
=
uut
.
as_binary
();
CHECK_EQ
(
bytes
.
size
(),
3u
);
CHECK_EQ
(
to_vec
(
bytes
),
buf
);
}
}
TEST_CASE
(
"construction from multiple byte buffers"
)
{
auto
buf1
=
to_byte_buf
(
1
,
2
);
auto
buf2
=
to_byte_buf
();
auto
buf3
=
to_byte_buf
(
3
,
4
,
5
);
auto
buf4
=
to_byte_buf
(
1
,
2
,
3
,
4
,
5
);
auto
uut
=
net
::
web_socket
::
frame
::
from_buffers
(
buf1
,
buf2
,
buf3
);
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK_EQ
(
uut
.
size
(),
5u
);
if
(
uut
.
is_binary
())
{
auto
bytes
=
uut
.
as_binary
();
CHECK_EQ
(
bytes
.
size
(),
5u
);
CHECK_EQ
(
to_vec
(
bytes
),
buf4
);
}
}
TEST_CASE
(
"construction from a single text buffer"
)
{
auto
uut
=
net
::
web_socket
::
frame
{
"foo"
sv
};
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK_EQ
(
uut
.
size
(),
3u
);
if
(
CHECK
(
uut
.
is_text
()))
{
CHECK_EQ
(
uut
.
as_text
(),
"foo"
sv
);
}
}
TEST_CASE
(
"construction from multiple text buffers"
)
{
auto
buf1
=
"foo"
sv
;
auto
buf2
=
""
sv
;
auto
buf3
=
"bar"
sv
;
auto
buf4
=
"foobar"
sv
;
auto
uut
=
net
::
web_socket
::
frame
::
from_strings
(
buf1
,
buf2
,
buf3
);
CHECK
(
static_cast
<
bool
>
(
uut
));
CHECK
(
!
uut
.
empty
());
CHECK_EQ
(
uut
.
size
(),
6u
);
if
(
uut
.
is_text
())
{
CHECK_EQ
(
uut
.
as_text
(),
buf4
);
}
}
TEST_CASE
(
"copying, moving and swapping"
)
{
auto
buf
=
to_byte_buf
(
1
,
2
,
3
);
auto
uut1
=
net
::
web_socket
::
frame
{};
auto
uut2
=
net
::
web_socket
::
frame
{
make_span
(
buf
)};
auto
uut3
=
uut1
;
auto
uut4
=
uut2
;
CHECK_EQ
(
uut1
.
as_binary
().
data
(),
uut3
.
as_binary
().
data
());
CHECK_EQ
(
uut2
.
as_binary
().
data
(),
uut4
.
as_binary
().
data
());
CHECK_NE
(
uut1
.
as_binary
().
data
(),
uut2
.
as_binary
().
data
());
auto
uut5
=
std
::
move
(
uut1
);
auto
uut6
=
std
::
move
(
uut2
);
CHECK_EQ
(
uut5
.
as_binary
().
data
(),
uut3
.
as_binary
().
data
());
CHECK_EQ
(
uut6
.
as_binary
().
data
(),
uut4
.
as_binary
().
data
());
CHECK_NE
(
uut5
.
as_binary
().
data
(),
uut6
.
as_binary
().
data
());
uut5
.
swap
(
uut6
);
CHECK_EQ
(
uut6
.
as_binary
().
data
(),
uut3
.
as_binary
().
data
());
CHECK_EQ
(
uut5
.
as_binary
().
data
(),
uut4
.
as_binary
().
data
());
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment