Commit 3f7c53b0 authored by Andy Wingo's avatar Andy Wingo Committed by V8 LUCI CQ

[stringrefs] Implement string.encode_wtf8_array

Bug: v8:12868
Change-Id: Ide772c6e480783931942f6c02eb3e57dd3adf508
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3751201
Commit-Queue: Andy Wingo <wingo@igalia.com>
Reviewed-by: 's avatarJakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#81597}
parent 0a8bd766
......@@ -50,6 +50,8 @@ extern runtime WasmStringMeasureUtf8(Context, String): Number;
extern runtime WasmStringMeasureWtf8(Context, String): Number;
extern runtime WasmStringEncodeWtf8(
Context, WasmInstanceObject, Smi, Smi, String, Number): Number;
extern runtime WasmStringEncodeWtf8Array(
Context, Smi, String, WasmArray, Number): Number;
extern runtime WasmStringEncodeWtf16(
Context, WasmInstanceObject, Smi, String, Number, Smi, Smi): JSAny;
}
......@@ -863,6 +865,14 @@ builtin WasmStringEncodeWtf8(
WasmUint32ToNumber(offset));
return ChangeNumberToUint32(result);
}
builtin WasmStringEncodeWtf8Array(
string: String, array: WasmArray, start: uint32, policy: Smi): uint32 {
const instance = LoadInstanceFromFrame();
const result = runtime::WasmStringEncodeWtf8Array(
LoadContextFromInstance(instance), policy, string, array,
WasmUint32ToNumber(start));
return ChangeNumberToUint32(result);
}
builtin WasmStringEncodeWtf16(
string: String, offset: uint32, memory: Smi): uint32 {
const instance = LoadInstanceFromFrame();
......
......@@ -5828,6 +5828,21 @@ Node* WasmGraphBuilder::StringEncodeWtf8(uint32_t memory,
gasm_->SmiConstant(policy));
}
Node* WasmGraphBuilder::StringEncodeWtf8Array(
wasm::StringRefWtf8Policy policy, Node* string,
CheckForNull string_null_check, Node* array, CheckForNull array_null_check,
Node* start, wasm::WasmCodePosition position) {
if (string_null_check == kWithNullCheck) {
string = AssertNotNull(string, position);
}
if (array_null_check == kWithNullCheck) {
array = AssertNotNull(array, position);
}
return gasm_->CallBuiltin(Builtin::kWasmStringEncodeWtf8Array,
Operator::kNoDeopt, string, array, start,
gasm_->SmiConstant(policy));
}
Node* WasmGraphBuilder::StringEncodeWtf16(uint32_t memory, Node* string,
CheckForNull null_check, Node* offset,
wasm::WasmCodePosition position) {
......
......@@ -551,6 +551,10 @@ class WasmGraphBuilder {
Node* StringEncodeWtf8(uint32_t memory, wasm::StringRefWtf8Policy policy,
Node* string, CheckForNull null_check, Node* offset,
wasm::WasmCodePosition position);
Node* StringEncodeWtf8Array(wasm::StringRefWtf8Policy policy, Node* string,
CheckForNull string_null_check, Node* array,
CheckForNull array_null_check, Node* start,
wasm::WasmCodePosition position);
Node* StringEncodeWtf16(uint32_t memory, Node* string,
CheckForNull null_check, Node* offset,
wasm::WasmCodePosition position);
......
......@@ -1020,14 +1020,14 @@ bool HasUnpairedSurrogate(base::Vector<const base::uc16> wtf16) {
}
// TODO(12868): Consider unifying with api.cc:String::WriteUtf8.
template <typename T>
int EncodeWtf8(char* memory_start, uint32_t offset, size_t mem_size,
int EncodeWtf8(base::Vector<char> bytes, size_t offset,
base::Vector<const T> wtf16, wasm::StringRefWtf8Policy policy,
MessageTemplate* message) {
MessageTemplate* message, MessageTemplate out_of_bounds) {
// The first check is a quick estimate to decide whether the second check
// is worth the computation.
if (!base::IsInBounds<size_t>(offset, MaxEncodedSize(wtf16), mem_size) &&
!base::IsInBounds<size_t>(offset, MeasureWtf8(wtf16), mem_size)) {
*message = MessageTemplate::kWasmTrapMemOutOfBounds;
if (!base::IsInBounds<size_t>(offset, MaxEncodedSize(wtf16), bytes.size()) &&
!base::IsInBounds<size_t>(offset, MeasureWtf8(wtf16), bytes.size())) {
*message = out_of_bounds;
return -1;
}
......@@ -1048,7 +1048,7 @@ int EncodeWtf8(char* memory_start, uint32_t offset, size_t mem_size,
UNREACHABLE();
}
char* dst_start = memory_start + offset;
char* dst_start = bytes.begin() + offset;
char* dst = dst_start;
int previous = unibrow::Utf16::kNoPreviousCharacter;
for (auto code_unit : wtf16) {
......@@ -1058,6 +1058,29 @@ int EncodeWtf8(char* memory_start, uint32_t offset, size_t mem_size,
DCHECK_LE(dst - dst_start, static_cast<ptrdiff_t>(kMaxInt));
return static_cast<int>(dst - dst_start);
}
template <typename GetWritableBytes>
Object EncodeWtf8(Isolate* isolate, wasm::StringRefWtf8Policy policy,
Handle<String> string, GetWritableBytes get_writable_bytes,
size_t offset, MessageTemplate out_of_bounds_message) {
string = String::Flatten(isolate, string);
MessageTemplate message;
int written;
{
DisallowGarbageCollection no_gc;
String::FlatContent content = string->GetFlatContent(no_gc);
base::Vector<char> dst = get_writable_bytes(no_gc);
written = content.IsOneByte()
? EncodeWtf8(dst, offset, content.ToOneByteVector(), policy,
&message, out_of_bounds_message)
: EncodeWtf8(dst, offset, content.ToUC16Vector(), policy,
&message, out_of_bounds_message);
}
if (written < 0) {
DCHECK_NE(message, MessageTemplate::kNone);
return ThrowWasmError(isolate, message);
}
return *isolate->factory()->NewNumberFromInt(written);
}
} // namespace
RUNTIME_FUNCTION(Runtime_WasmStringMeasureUtf8) {
......@@ -1123,26 +1146,32 @@ RUNTIME_FUNCTION(Runtime_WasmStringEncodeWtf8) {
DCHECK(policy_value <= wasm::kLastWtf8Policy);
char* memory_start = reinterpret_cast<char*>(instance.memory_start());
size_t mem_size = instance.memory_size();
auto policy = static_cast<wasm::StringRefWtf8Policy>(policy_value);
auto get_writable_bytes =
[&](const DisallowGarbageCollection&) -> base::Vector<char> {
return {memory_start, instance.memory_size()};
};
return EncodeWtf8(isolate, policy, string, get_writable_bytes, offset,
MessageTemplate::kWasmTrapMemOutOfBounds);
}
string = String::Flatten(isolate, string);
MessageTemplate message;
int written;
{
DisallowGarbageCollection no_gc;
String::FlatContent content = string->GetFlatContent(no_gc);
written = content.IsOneByte()
? EncodeWtf8(memory_start, offset, mem_size,
content.ToOneByteVector(), policy, &message)
: EncodeWtf8(memory_start, offset, mem_size,
content.ToUC16Vector(), policy, &message);
}
if (written < 0) {
DCHECK_NE(message, MessageTemplate::kNone);
return ThrowWasmError(isolate, message);
}
return *isolate->factory()->NewNumberFromInt(written);
RUNTIME_FUNCTION(Runtime_WasmStringEncodeWtf8Array) {
ClearThreadInWasmScope flag_scope(isolate);
DCHECK_EQ(4, args.length());
HandleScope scope(isolate);
uint32_t policy_value = args.positive_smi_value_at(0);
Handle<String> string(String::cast(args[1]), isolate);
Handle<WasmArray> array(WasmArray::cast(args[2]), isolate);
uint32_t start = NumberToUint32(args[3]);
DCHECK(policy_value <= wasm::kLastWtf8Policy);
auto policy = static_cast<wasm::StringRefWtf8Policy>(policy_value);
auto get_writable_bytes =
[&](const DisallowGarbageCollection&) -> base::Vector<char> {
return {reinterpret_cast<char*>(array->ElementAddress(0)), array->length()};
};
return EncodeWtf8(isolate, policy, string, get_writable_bytes, start,
MessageTemplate::kWasmTrapArrayOutOfBounds);
}
RUNTIME_FUNCTION(Runtime_WasmStringEncodeWtf16) {
......
......@@ -617,7 +617,8 @@ namespace internal {
F(WasmStringMeasureUtf8, 1, 1) \
F(WasmStringMeasureWtf8, 1, 1) \
F(WasmStringEncodeWtf8, 5, 1) \
F(WasmStringEncodeWtf16, 6, 1)
F(WasmStringEncodeWtf16, 6, 1) \
F(WasmStringEncodeWtf8Array, 4, 1)
#define FOR_EACH_INTRINSIC_WASM_TEST(F, I) \
F(DeserializeWasmModule, 2, 1) \
......
......@@ -6400,7 +6400,40 @@ class LiftoffCompiler {
const Wtf8PolicyImmediate<validate>& imm,
const Value& str, const Value& array,
const Value& start, Value* result) {
UNIMPLEMENTED();
LiftoffRegList pinned;
LiftoffRegister array_reg = pinned.set(
__ LoadToRegister(__ cache_state()->stack_state.end()[-2], pinned));
MaybeEmitNullCheck(decoder, array_reg.gp(), pinned, array.type);
LiftoffAssembler::VarState array_var(kRef, array_reg, 0);
LiftoffRegister string_reg = pinned.set(
__ LoadToRegister(__ cache_state()->stack_state.end()[-3], pinned));
MaybeEmitNullCheck(decoder, string_reg.gp(), pinned, str.type);
LiftoffAssembler::VarState string_var(kRef, string_reg, 0);
LiftoffAssembler::VarState& start_var =
__ cache_state()->stack_state.end()[-1];
LiftoffRegister policy_reg =
pinned.set(__ GetUnusedRegister(kGpReg, pinned));
LoadSmi(policy_reg, static_cast<int32_t>(imm.value));
LiftoffAssembler::VarState policy_var(kSmiKind, policy_reg, 0);
CallRuntimeStub(WasmCode::kWasmStringEncodeWtf8Array,
MakeSig::Returns(kI32).Params(kRef, kRef, kI32, kSmiKind),
{
string_var,
array_var,
start_var,
policy_var,
},
decoder->position());
__ DropValues(3);
RegisterDebugSideTableEntry(decoder, DebugSideTableBuilder::kDidSpill);
LiftoffRegister result_reg(kReturnRegister0);
__ PushRegister(kI32, result_reg);
}
void StringEncodeWtf16(FullDecoder* decoder,
......
......@@ -5233,6 +5233,8 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
}
}
enum class WasmArrayAccess { kRead, kWrite };
int DecodeStringRefOpcode(WasmOpcode opcode, uint32_t opcode_length) {
switch (opcode) {
case kExprStringNewWtf8: {
......@@ -5513,7 +5515,7 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
CHECK_PROTOTYPE_OPCODE(gc);
NON_CONST_ONLY
Wtf8PolicyImmediate<validate> imm(this, this->pc_ + opcode_length);
Value array = PeekPackedArray(2, 0, kWasmI8);
Value array = PeekPackedArray(2, 0, kWasmI8, WasmArrayAccess::kRead);
Value start = Peek(1, 1, kWasmI32);
Value end = Peek(0, 2, kWasmI32);
Value result = CreateValue(kWasmStringRef);
......@@ -5526,7 +5528,7 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
case kExprStringNewWtf16Array: {
CHECK_PROTOTYPE_OPCODE(gc);
NON_CONST_ONLY
Value array = PeekPackedArray(2, 0, kWasmI16);
Value array = PeekPackedArray(2, 0, kWasmI16, WasmArrayAccess::kRead);
Value start = Peek(1, 1, kWasmI32);
Value end = Peek(0, 2, kWasmI32);
Value result = CreateValue(kWasmStringRef);
......@@ -5541,7 +5543,7 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
NON_CONST_ONLY
Wtf8PolicyImmediate<validate> imm(this, this->pc_ + opcode_length);
Value str = Peek(2, 0, kWasmStringRef);
Value array = PeekPackedArray(1, 1, kWasmI8);
Value array = PeekPackedArray(1, 1, kWasmI8, WasmArrayAccess::kWrite);
Value start = Peek(0, 2, kWasmI32);
Value result = CreateValue(kWasmI32);
CALL_INTERFACE_IF_OK_AND_REACHABLE(StringEncodeWtf8Array, imm, str,
......@@ -5554,7 +5556,7 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
CHECK_PROTOTYPE_OPCODE(gc);
NON_CONST_ONLY
Value str = Peek(2, 0, kWasmStringRef);
Value array = PeekPackedArray(1, 1, kWasmI16);
Value array = PeekPackedArray(1, 1, kWasmI16, WasmArrayAccess::kWrite);
Value start = Peek(0, 2, kWasmI32);
Value result = CreateValue(kWasmI32);
CALL_INTERFACE_IF_OK_AND_REACHABLE(StringEncodeWtf16Array, str, array,
......@@ -5868,7 +5870,8 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
}
Value PeekPackedArray(uint32_t stack_depth, uint32_t operand_index,
ValueType expected_element_type) {
ValueType expected_element_type,
WasmArrayAccess access) {
Value array = Peek(stack_depth);
if (array.type.is_bottom()) {
// We are in a polymorphic stack. Leave the stack as it is.
......@@ -5877,14 +5880,20 @@ class WasmFullDecoder : public WasmDecoder<validate, decoding_mode> {
}
if (VALIDATE(array.type.is_object_reference() && array.type.has_index())) {
uint32_t ref_index = array.type.ref_index();
if (VALIDATE(this->module_->has_array(ref_index) &&
this->module_->array_type(ref_index)->element_type() ==
expected_element_type)) {
return array;
if (VALIDATE(this->module_->has_array(ref_index))) {
const ArrayType* array_type = this->module_->array_type(ref_index);
if (VALIDATE(array_type->element_type() == expected_element_type &&
(access == WasmArrayAccess::kRead ||
array_type->mutability()))) {
return array;
}
}
}
PopTypeError(operand_index, array,
("array of " + expected_element_type.name()).c_str());
(std::string("array of ") +
(access == WasmArrayAccess::kWrite ? "mutable " : "") +
expected_element_type.name())
.c_str());
return array;
}
......
......@@ -1383,34 +1383,36 @@ class WasmGraphBuildingInterface {
void StringNewWtf8(FullDecoder* decoder,
const EncodeWtf8Immediate<validate>& imm,
const Value& offset, const Value& size, Value* result) {
result->node = builder_->StringNewWtf8(imm.memory.index, imm.policy.value,
offset.node, size.node);
SetAndTypeNode(result,
builder_->StringNewWtf8(imm.memory.index, imm.policy.value,
offset.node, size.node));
}
void StringNewWtf8Array(FullDecoder* decoder,
const Wtf8PolicyImmediate<validate>& imm,
const Value& array, const Value& start,
const Value& end, Value* result) {
result->node = builder_->StringNewWtf8Array(imm.value, array.node,
start.node, end.node);
SetAndTypeNode(result, builder_->StringNewWtf8Array(imm.value, array.node,
start.node, end.node));
}
void StringNewWtf16(FullDecoder* decoder,
const MemoryIndexImmediate<validate>& imm,
const Value& offset, const Value& size, Value* result) {
result->node = builder_->StringNewWtf16(imm.index, offset.node, size.node);
SetAndTypeNode(result,
builder_->StringNewWtf16(imm.index, offset.node, size.node));
}
void StringNewWtf16Array(FullDecoder* decoder, const Value& array,
const Value& start, const Value& end,
Value* result) {
result->node =
builder_->StringNewWtf16Array(array.node, start.node, end.node);
SetAndTypeNode(result, builder_->StringNewWtf16Array(array.node, start.node,
end.node));
}
void StringConst(FullDecoder* decoder,
const StringConstImmediate<validate>& imm, Value* result) {
result->node = builder_->StringConst(imm.index);
SetAndTypeNode(result, builder_->StringConst(imm.index));
}
void StringMeasureWtf8(FullDecoder* decoder,
......@@ -1447,7 +1449,9 @@ class WasmGraphBuildingInterface {
const Wtf8PolicyImmediate<validate>& imm,
const Value& str, const Value& array,
const Value& start, Value* result) {
UNIMPLEMENTED();
result->node = builder_->StringEncodeWtf8Array(
imm.value, str.node, NullCheckFor(str.type), array.node,
NullCheckFor(array.type), start.node, decoder->position());
}
void StringEncodeWtf16(FullDecoder* decoder,
......@@ -1466,9 +1470,9 @@ class WasmGraphBuildingInterface {
void StringConcat(FullDecoder* decoder, const Value& head, const Value& tail,
Value* result) {
result->node =
builder_->StringConcat(head.node, NullCheckFor(head.type), tail.node,
NullCheckFor(tail.type), decoder->position());
SetAndTypeNode(result, builder_->StringConcat(
head.node, NullCheckFor(head.type), tail.node,
NullCheckFor(tail.type), decoder->position()));
}
void StringEq(FullDecoder* decoder, const Value& a, const Value& b,
......@@ -1527,9 +1531,9 @@ class WasmGraphBuildingInterface {
void StringViewWtf16Slice(FullDecoder* decoder, const Value& view,
const Value& start, const Value& end,
Value* result) {
result->node = builder_->StringViewWtf16Slice(
view.node, NullCheckFor(view.type), start.node, end.node,
decoder->position());
SetAndTypeNode(result, builder_->StringViewWtf16Slice(
view.node, NullCheckFor(view.type), start.node,
end.node, decoder->position()));
}
void StringAsIter(FullDecoder* decoder, const Value& str, Value* result) {
......
......@@ -137,7 +137,8 @@ struct WasmModule;
V(WasmStringViewWtf16Encode) \
V(WasmStringViewWtf16Slice) \
V(WasmStringNewWtf8Array) \
V(WasmStringNewWtf16Array)
V(WasmStringNewWtf16Array) \
V(WasmStringEncodeWtf8Array)
// Sorted, disjoint and non-overlapping memory regions. A region is of the
// form [start, end). So there's no [start, end), [end, other_end),
......
......@@ -257,3 +257,108 @@ function makeWtf16TestDataSegment() {
"ascii".length + 1),
WebAssembly.RuntimeError, "array element access out of bounds");
})();
(function TestStringEncodeWtf8Array() {
let builder = new WasmModuleBuilder();
let i8_array = builder.addArray(kWasmI8, true);
let kSig_w_wii =
makeSig([kWasmStringRef, kWasmI32, kWasmI32],
[kWasmStringRef]);
for (let [policy, name] of ["utf8", "wtf8", "replace"].entries()) {
// Allocate an array that's exactly the expected size, and encode
// into it. Then decode it.
// (str, length, offset=0) -> str
builder.addFunction("encode_" + name, kSig_w_wii)
.exportFunc()
.addLocals(wasmRefNullType(i8_array), 1)
.addLocals(kWasmI32, 1)
.addBody([
// Allocate buffer.
kExprLocalGet, 1,
kGCPrefix, kExprArrayNewDefault, i8_array,
kExprLocalSet, 3,
// Write buffer, store number of bytes written.
kExprLocalGet, 0,
kExprLocalGet, 3,
kExprLocalGet, 2,
kGCPrefix, kExprStringEncodeWtf8Array, policy,
kExprLocalSet, 4,
// Read buffer.
kExprLocalGet, 3,
kExprLocalGet, 2,
kExprLocalGet, 2, kExprLocalGet, 4, kExprI32Add,
kGCPrefix, kExprStringNewWtf8Array, kWtf8PolicyAccept,
]);
}
builder.addFunction("encode_null_string", kSig_i_v)
.exportFunc()
.addBody([
kExprRefNull, kStringRefCode,
kExprI32Const, 0, kGCPrefix, kExprArrayNewDefault, i8_array,
kExprI32Const, 0,
kGCPrefix, kExprStringEncodeWtf8Array, 0,
]);
builder.addFunction("encode_null_array", kSig_i_v)
.exportFunc()
.addBody([
kExprI32Const, 0, kGCPrefix, kExprArrayNewDefault, i8_array,
kExprI32Const, 0, kExprI32Const, 0,
kGCPrefix, kExprStringNewWtf8Array, kWtf8PolicyAccept,
kExprRefNull, i8_array,
kExprI32Const, 0,
kGCPrefix, kExprStringEncodeWtf8Array, kWtf8PolicyAccept,
]);
let instance = builder.instantiate();
for (let str of interestingStrings) {
let wtf8 = encodeWtf8(str);
assertEquals(str, instance.exports.encode_wtf8(str, wtf8.length, 0));
assertEquals(str, instance.exports.encode_wtf8(str, wtf8.length + 20,
10));
}
for (let str of interestingStrings) {
let wtf8 = encodeWtf8(str);
if (HasIsolatedSurrogate(str)) {
assertThrows(() => instance.exports.encode_utf8(str, wtf8.length, 0),
WebAssembly.RuntimeError,
"Failed to encode string as UTF-8: contains unpaired surrogate");
} else {
assertEquals(str, instance.exports.encode_utf8(str, wtf8.length, 0));
assertEquals(str,
instance.exports.encode_wtf8(str, wtf8.length + 20, 10));
}
}
for (let str of interestingStrings) {
let offset = 42;
let replaced = ReplaceIsolatedSurrogates(str);
if (!HasIsolatedSurrogate(str)) assertEquals(str, replaced);
let wtf8 = encodeWtf8(replaced);
assertEquals(replaced,
instance.exports.encode_replace(str, wtf8.length, 0));
assertEquals(replaced,
instance.exports.encode_replace(str, wtf8.length + 20, 10));
}
assertThrows(() => instance.exports.encode_null_array(),
WebAssembly.RuntimeError, "dereferencing a null pointer");
assertThrows(() => instance.exports.encode_null_string(),
WebAssembly.RuntimeError, "dereferencing a null pointer");
for (let str of interestingStrings) {
let wtf8 = encodeWtf8(str);
let message = "array element access out of bounds";
assertThrows(() => instance.exports.encode_wtf8(str, wtf8.length, 1),
WebAssembly.RuntimeError, message);
assertThrows(() => instance.exports.encode_utf8(str, wtf8.length, 1),
WebAssembly.RuntimeError, message);
assertThrows(() => instance.exports.encode_replace(str, wtf8.length, 1),
WebAssembly.RuntimeError, message);
}
})();
......@@ -449,3 +449,41 @@ assertInvalid(
"Compiling function #0:\"string.new_wtf16_array/bad-type\" failed: " +
"string.new_wtf16_array[0] expected array of i16, " +
"found ref.null of type (ref null 0) @+27");
assertInvalid(
builder => {
let immutable_i8_array = builder.addArray(kWasmI8, false);
let sig = makeSig([kWasmStringRef,
wasmRefType(immutable_i8_array),
kWasmI32],
[kWasmI32]);
builder.addFunction("string.encode_wtf8_array/bad-type", sig)
.addBody([
kExprLocalGet, 0,
kExprLocalGet, 1,
kExprLocalGet, 2,
kGCPrefix, kExprStringEncodeWtf8Array, kWtf8PolicyAccept,
]);
},
"Compiling function #0:\"string.encode_wtf8_array/bad-type\" failed: " +
"string.encode_wtf8_array[1] expected array of mutable i8, " +
"found local.get of type (ref 0) @+33");
assertInvalid(
builder => {
let immutable_i16_array = builder.addArray(kWasmI16, false);
let sig = makeSig([kWasmStringRef,
wasmRefType(immutable_i16_array),
kWasmI32],
[kWasmI32]);
builder.addFunction("string.encode_wtf16_array/bad-type", sig)
.addBody([
kExprLocalGet, 0,
kExprLocalGet, 1,
kExprLocalGet, 2,
kGCPrefix, kExprStringEncodeWtf16Array,
]);
},
"Compiling function #0:\"string.encode_wtf16_array/bad-type\" failed: " +
"string.encode_wtf16_array[1] expected array of mutable i16, " +
"found local.get of type (ref 0) @+33");
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment