Skip to content

Commit 9b5b4d8

Browse files
authored
Serialize (and deserialize) string values (firebase#864)
1 parent c6f73f7 commit 9b5b4d8

File tree

3 files changed

+122
-1
lines changed

3 files changed

+122
-1
lines changed

Firestore/core/src/firebase/firestore/model/field_value.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ class FieldValue {
106106
return integer_value_;
107107
}
108108

109+
const std::string& string_value() const {
110+
FIREBASE_ASSERT(tag_ == Type::String);
111+
return string_value_;
112+
}
113+
109114
/** factory methods. */
110115
static const FieldValue& NullValue();
111116
static const FieldValue& TrueValue();

Firestore/core/src/firebase/firestore/remote/serializer.cc

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include <pb_decode.h>
2020
#include <pb_encode.h>
2121

22+
#include <string>
23+
2224
namespace firebase {
2325
namespace firestore {
2426
namespace remote {
@@ -98,6 +100,47 @@ int64_t DecodeInteger(pb_istream_t* stream) {
98100
return DecodeVarint(stream);
99101
}
100102

103+
void EncodeString(pb_ostream_t* stream, const std::string& string_value) {
104+
bool status = pb_encode_string(
105+
stream, reinterpret_cast<const pb_byte_t*>(string_value.c_str()),
106+
string_value.length());
107+
if (!status) {
108+
// TODO(rsgowman): figure out error handling
109+
abort();
110+
}
111+
}
112+
113+
std::string DecodeString(pb_istream_t* stream) {
114+
pb_istream_t substream;
115+
bool status = pb_make_string_substream(stream, &substream);
116+
if (!status) {
117+
// TODO(rsgowman): figure out error handling
118+
abort();
119+
}
120+
121+
std::string result(substream.bytes_left, '\0');
122+
status = pb_read(&substream, reinterpret_cast<pb_byte_t*>(&result[0]),
123+
substream.bytes_left);
124+
if (!status) {
125+
// TODO(rsgowman): figure out error handling
126+
abort();
127+
}
128+
129+
// NB: future versions of nanopb read the remaining characters out of the
130+
// substream (and return false if that fails) as an additional safety
131+
// check within pb_close_string_substream. Unfortunately, that's not present
132+
// in the current version (0.38). We'll make a stronger assertion and check
133+
// to make sure there *are* no remaining characters in the substream.
134+
if (substream.bytes_left != 0) {
135+
// TODO(rsgowman): figure out error handling
136+
abort();
137+
}
138+
139+
pb_close_string_substream(stream, &substream);
140+
141+
return result;
142+
}
143+
101144
} // namespace
102145

103146
using firebase::firestore::model::FieldValue;
@@ -144,6 +187,16 @@ void Serializer::EncodeFieldValue(const FieldValue& field_value,
144187
EncodeInteger(&stream, field_value.integer_value());
145188
break;
146189

190+
case FieldValue::Type::String:
191+
status = pb_encode_tag(&stream, PB_WT_STRING,
192+
google_firestore_v1beta1_Value_string_value_tag);
193+
if (!status) {
194+
// TODO(rsgowman): figure out error handling
195+
abort();
196+
}
197+
EncodeString(&stream, field_value.string_value());
198+
break;
199+
147200
default:
148201
// TODO(rsgowman): implement the other types
149202
abort();
@@ -158,11 +211,32 @@ FieldValue Serializer::DecodeFieldValue(const uint8_t* bytes, size_t length) {
158211
uint32_t tag;
159212
bool eof;
160213
bool status = pb_decode_tag(&stream, &wire_type, &tag, &eof);
161-
if (!status || wire_type != PB_WT_VARINT) {
214+
if (!status) {
162215
// TODO(rsgowman): figure out error handling
163216
abort();
164217
}
165218

219+
// Ensure the tag matches the wire type
220+
// TODO(rsgowman): figure out error handling
221+
switch (tag) {
222+
case google_firestore_v1beta1_Value_null_value_tag:
223+
case google_firestore_v1beta1_Value_boolean_value_tag:
224+
case google_firestore_v1beta1_Value_integer_value_tag:
225+
if (wire_type != PB_WT_VARINT) {
226+
abort();
227+
}
228+
break;
229+
230+
case google_firestore_v1beta1_Value_string_value_tag:
231+
if (wire_type != PB_WT_STRING) {
232+
abort();
233+
}
234+
break;
235+
236+
default:
237+
abort();
238+
}
239+
166240
switch (tag) {
167241
case google_firestore_v1beta1_Value_null_value_tag:
168242
DecodeNull(&stream);
@@ -171,6 +245,8 @@ FieldValue Serializer::DecodeFieldValue(const uint8_t* bytes, size_t length) {
171245
return FieldValue::BooleanValue(DecodeBool(&stream));
172246
case google_firestore_v1beta1_Value_integer_value_tag:
173247
return FieldValue::IntegerValue(DecodeInteger(&stream));
248+
case google_firestore_v1beta1_Value_string_value_tag:
249+
return FieldValue::StringValue(DecodeString(&stream));
174250

175251
default:
176252
// TODO(rsgowman): figure out error handling

Firestore/core/test/firebase/firestore/remote/serializer_test.cc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,46 @@ TEST_F(SerializerTest, EncodesIntegersModelToBytes) {
151151
}
152152
}
153153

154+
TEST_F(SerializerTest, EncodesStringModelToBytes) {
155+
struct TestCase {
156+
std::string value;
157+
std::vector<uint8_t> bytes;
158+
};
159+
160+
std::vector<TestCase> cases{
161+
// TEXT_FORMAT_PROTO: 'string_value: ""'
162+
{"", {0x8a, 0x01, 0x00}},
163+
// TEXT_FORMAT_PROTO: 'string_value: "a"'
164+
{"a", {0x8a, 0x01, 0x01, 0x61}},
165+
// TEXT_FORMAT_PROTO: 'string_value: "abc def"'
166+
{"abc def", {0x8a, 0x01, 0x07, 0x61, 0x62, 0x63, 0x20, 0x64, 0x65, 0x66}},
167+
// TEXT_FORMAT_PROTO: 'string_value: "æ"'
168+
{"æ", {0x8a, 0x01, 0x02, 0xc3, 0xa6}},
169+
// TEXT_FORMAT_PROTO: 'string_value: "\0\ud7ff\ue000\uffff"'
170+
// Note: Each one of the three embedded universal character names
171+
// (\u-escaped) maps to three chars, so the total length of the string
172+
// literal is 10 (ignoring the terminating null), and the resulting string
173+
// literal is the same as '\0\xed\x9f\xbf\xee\x80\x80\xef\xbf\xbf'". The
174+
// size of 10 must be added, or else std::string will see the \0 at the
175+
// start and assume that's the end of the string.
176+
{{"\0\ud7ff\ue000\uffff", 10},
177+
{0x8a, 0x01, 0x0a, 0x00, 0xed, 0x9f, 0xbf, 0xee, 0x80, 0x80, 0xef, 0xbf,
178+
0xbf}},
179+
{{"\0\xed\x9f\xbf\xee\x80\x80\xef\xbf\xbf", 10},
180+
{0x8a, 0x01, 0x0a, 0x00, 0xed, 0x9f, 0xbf, 0xee, 0x80, 0x80, 0xef, 0xbf,
181+
0xbf}},
182+
// TEXT_FORMAT_PROTO: 'string_value: "(╯°□°)╯︵ ┻━┻"'
183+
{"(╯°□°)╯︵ ┻━┻",
184+
{0x8a, 0x01, 0x1e, 0x28, 0xe2, 0x95, 0xaf, 0xc2, 0xb0, 0xe2, 0x96,
185+
0xa1, 0xc2, 0xb0, 0xef, 0xbc, 0x89, 0xe2, 0x95, 0xaf, 0xef, 0xb8,
186+
0xb5, 0x20, 0xe2, 0x94, 0xbb, 0xe2, 0x94, 0x81, 0xe2, 0x94, 0xbb}}};
187+
188+
for (const TestCase& test : cases) {
189+
FieldValue model = FieldValue::StringValue(test.value);
190+
ExpectRoundTrip(model, test.bytes, FieldValue::Type::String);
191+
}
192+
}
193+
154194
// TODO(rsgowman): Test [en|de]coding multiple protos into the same output
155195
// vector.
156196

0 commit comments

Comments
 (0)