Skip to content

Commit e419899

Browse files
wilhuffjonsimantov
authored andcommitted
Fix Unicode handling on Android
The JNI support for Java Strings added in cl/321475293 did not handle Unicode characters correctly, passing the number of bytes requested to a method that actually wanted the number of chars. Switching to this broke reading strings from Java if they have any non-ASCII Unicode characters in them. Additionally, the old routines cl/321475293 was supposed to replace treat the bytes coming into/out of Java as modified UTF-8, even though our product works with standard UTF-8. This meant we had latent bugs around handling of null bytes and characters beyond the basic multilingual plane. This CL fixes both issues, by converting Strings to UTF-8 bytes using methods on String itself. This avoids dealing in modified UTF-8 or the bug prone JNI methods like `GetStringUTFRegion`. PiperOrigin-RevId: 348050717
1 parent 5b84e36 commit e419899

File tree

12 files changed

+177
-85
lines changed

12 files changed

+177
-85
lines changed

firestore/src/android/firestore_android.cc

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,14 +232,15 @@ bool FirestoreInternal::Initialize(App* app) {
232232
FIREBASE_DEV_ASSERT(java_firestores == nullptr);
233233
java_firestores = new JavaFirestoreMap();
234234

235+
Env env = GetEnv();
235236
Loader loader(app);
236237
loader.AddEmbeddedFile(::firebase_firestore::firestore_resources_filename,
237238
::firebase_firestore::firestore_resources_data,
238239
::firebase_firestore::firestore_resources_size);
239240
loader.CacheEmbeddedFiles();
240241

241242
jni::Object::Initialize(loader);
242-
243+
jni::String::Initialize(env, loader);
243244
jni::ArrayList::Initialize(loader);
244245
jni::Boolean::Initialize(loader);
245246
jni::Collection::Initialize(loader);
@@ -279,30 +280,27 @@ bool FirestoreInternal::Initialize(App* app) {
279280
TransactionInternal::Initialize(loader);
280281
WriteBatchInternal::Initialize(loader);
281282
if (!loader.ok()) {
282-
ReleaseClassesLocked(app);
283+
ReleaseClassesLocked(env);
283284
return false;
284285
}
285286

286287
FIREBASE_DEV_ASSERT(global_loader == nullptr);
287288
global_loader = new Loader(Move(loader));
288289

289290
if (initial_log_state != InitialLogState::kUnset) {
290-
Env env;
291291
bool enabled = initial_log_state == InitialLogState::kSetEnabled;
292292
env.Call(kSetLoggingEnabled, enabled);
293-
294-
// If this fails it's unimportant. If there's some broad systemic failure
295-
// the next real operation will trigger it too.
296-
env.ExceptionClear();
297293
}
298294
}
299295
initialize_count++;
300296
return true;
301297
}
302298

303299
/* static */
304-
void FirestoreInternal::ReleaseClassesLocked(App* app) {
300+
void FirestoreInternal::ReleaseClassesLocked(Env& env) {
305301
// Assumes `init_mutex` is held.
302+
String::Terminate(env);
303+
306304
delete global_loader;
307305
global_loader = nullptr;
308306
}
@@ -313,7 +311,9 @@ void FirestoreInternal::Terminate(App* app) {
313311
FIREBASE_ASSERT(initialize_count > 0);
314312
initialize_count--;
315313
if (initialize_count == 0) {
316-
ReleaseClassesLocked(app);
314+
Env env(app->GetJNIEnv());
315+
ReleaseClassesLocked(env);
316+
317317
delete java_firestores;
318318
java_firestores = nullptr;
319319
}

firestore/src/android/firestore_android.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class FirestoreInternal {
178178
void ShutdownUserCallbackExecutor(jni::Env& env);
179179

180180
static bool Initialize(App* app);
181-
static void ReleaseClassesLocked(App* app);
181+
static void ReleaseClassesLocked(jni::Env& env);
182182
static void Terminate(App* app);
183183

184184
jni::Global<jni::Object> user_callback_executor_;

firestore/src/jni/env.cc

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "app/src/assert.h"
44
#include "app/src/log.h"
5+
#include "firestore/src/jni/array.h"
56

67
// Add constant declarations missing from the NDK's jni.h
78
#ifndef JNI_ENOMEM
@@ -144,28 +145,36 @@ jmethodID Env::GetStaticMethodId(const Class& clazz, const char* name,
144145
}
145146

146147
Local<String> Env::NewStringUtf(const char* bytes) {
148+
if (!bytes) return {};
149+
150+
return NewStringUtf(bytes, strlen(bytes));
151+
}
152+
153+
Local<String> Env::NewStringUtf(const char* bytes, size_t size) {
147154
if (!ok()) return {};
148155

149-
jstring result = env_->NewStringUTF(bytes);
150-
RecordException();
151-
return Local<String>(env_, result);
156+
Local<Array<uint8_t>> java_bytes = NewArray<uint8_t>(size);
157+
SetArrayRegion(java_bytes, 0, size, reinterpret_cast<const uint8_t*>(bytes));
158+
if (!ok()) return {};
159+
160+
return String::Create(*this, java_bytes, String::GetUtf8());
152161
}
153162

154-
std::string Env::GetStringUtfRegion(const String& string, size_t start,
155-
size_t len) {
163+
std::string Env::ToStringUtf(const String& string) {
156164
if (!ok()) return "";
157165

166+
Local<Array<uint8_t>> bytes = string.GetBytes(*this, String::GetUtf8());
167+
size_t len = GetArrayLength(bytes);
168+
158169
// Copy directly into the std::string buffer. This is guaranteed to work as
159170
// of C++11, and also happens to work with STLPort.
160171
std::string result;
161172
result.resize(len);
162173

163-
env_->GetStringUTFRegion(string.get(), ToJni(start), ToJni(len), &result[0]);
164-
RecordException();
165-
166-
// Ensure that if there was an exception, the contents of the buffer are
167-
// disregarded.
174+
auto* buffer = reinterpret_cast<uint8_t*>(&result[0]);
175+
GetArrayRegion(bytes, 0, len, buffer);
168176
if (!ok()) return "";
177+
169178
return result;
170179
}
171180

firestore/src/jni/env.h

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -297,29 +297,28 @@ class Env {
297297
// MARK: String Operations
298298

299299
/**
300-
* Creates a new proxy for a Java String from a sequences of modified UTF-8
301-
* bytes.
300+
* Creates a new proxy for a Java String from a sequence of UTF-8 bytes.
301+
*
302+
* Note that unlike the underlying JNI method, these bytes should be encoded
303+
* in standard UTF-8, and *not* in the modified UTF-8 customarily used in the
304+
* JNI API.
305+
*
306+
* @param bytes The UTF-8 bytes from which to create a Java String.
307+
* @param size The number of bytes in the UTF-8 sequence.
302308
*/
303309
Local<String> NewStringUtf(const char* bytes);
310+
Local<String> NewStringUtf(const char* bytes, size_t size);
304311
Local<String> NewStringUtf(const std::string& bytes) {
305-
return NewStringUtf(bytes.c_str());
306-
}
307-
308-
/** Returns the length of the string in modified UTF-8 bytes. */
309-
size_t GetStringUtfLength(const String& string) {
310-
if (!ok()) return 0;
311-
312-
jsize result = env_->GetStringUTFLength(string.get());
313-
RecordException();
314-
return static_cast<size_t>(result);
312+
return NewStringUtf(bytes.c_str(), bytes.size());
315313
}
316314

317315
/**
318-
* Copies the contents of a region of a Java string to a C++ string. The
319-
* resulting string has a modified UTF-8 encoding.
316+
* Converts the given Java String to a C++ string encoded in UTF-8.
317+
*
318+
* Note that this string is encoded in standard UTF-8, and *not* in the
319+
* modified UTF-8 customarily used in the JNI API.
320320
*/
321-
std::string GetStringUtfRegion(const String& string, size_t start,
322-
size_t len);
321+
std::string ToStringUtf(const String& string);
323322

324323
// MARK: Array Operations
325324

firestore/src/jni/loader.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Loader {
4444
* Returns true if the loader has succeeded. If not, any errors have already
4545
* been logged.
4646
*/
47-
bool ok() const { return ok_; }
47+
bool ok() const { return ok_ && !env_->ExceptionCheck(); }
4848

4949
/**
5050
* Adds metadata about embedded class files in the binary distribution.

firestore/src/jni/object.cc

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,24 @@ namespace jni {
1111
namespace {
1212

1313
Method<bool> kEquals("equals", "(Ljava/lang/Object;)Z");
14-
jclass g_clazz = nullptr;
14+
Method<String> kToString("toString", "()Ljava/lang/String;");
15+
jclass object_class = nullptr;
1516

1617
} // namespace
1718

1819
void Object::Initialize(Loader& loader) {
19-
g_clazz = util::object::GetClass();
20-
loader.LoadFromExistingClass("java/lang/Object", g_clazz, kEquals);
20+
object_class = util::object::GetClass();
21+
loader.LoadFromExistingClass("java/lang/Object", object_class, kEquals,
22+
kToString);
2123
}
2224

23-
Class Object::GetClass() { return Class(util::object::GetClass()); }
25+
Class Object::GetClass() { return Class(object_class); }
2426

25-
std::string Object::ToString(JNIEnv* env) const {
26-
return util::JniObjectToString(env, object_);
27+
std::string Object::ToString(Env& env) const {
28+
Local<String> java_string = env.Call(*this, kToString);
29+
return java_string.ToString(env);
2730
}
2831

29-
std::string Object::ToString(Env& env) const { return ToString(env.get()); }
30-
3132
bool Object::Equals(Env& env, const Object& other) const {
3233
return env.Call(*this, kEquals, other);
3334
}

firestore/src/jni/object.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ class Object {
4343
}
4444

4545
/**
46-
* Converts this object to a C++ String by calling the Java `toString` method
47-
* on it.
46+
* Converts this object to a C++ String encoded in UTF-8 by calling the Java
47+
* `toString` method on it.
48+
*
49+
* Note that this string is encoded in standard UTF-8, and *not* in the
50+
* modified UTF-8 customarily used in the JNI API.
4851
*/
49-
std::string ToString(JNIEnv* env) const;
5052
std::string ToString(Env& env) const;
5153

5254
bool Equals(Env& env, const Object& other) const;

firestore/src/jni/string.cc

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,60 @@
11
#include "firestore/src/jni/string.h"
22

3+
#include <cstring>
4+
5+
#include "app/src/log.h"
36
#include "app/src/util_android.h"
7+
#include "firestore/src/jni/array.h"
48
#include "firestore/src/jni/class.h"
59
#include "firestore/src/jni/env.h"
10+
#include "firestore/src/jni/loader.h"
611

712
namespace firebase {
813
namespace firestore {
914
namespace jni {
15+
namespace {
16+
17+
Constructor<String> kNewFromBytes("([BLjava/lang/String;)V");
18+
Method<Array<uint8_t>> kGetBytes("getBytes", "(Ljava/lang/String;)[B");
19+
jclass string_class = nullptr;
20+
jstring utf8_string = nullptr;
21+
22+
} // namespace
23+
24+
void String::Initialize(Env& env, Loader& loader) {
25+
string_class = util::string::GetClass();
26+
loader.LoadFromExistingClass("java/lang/String", string_class, kNewFromBytes,
27+
kGetBytes);
1028

11-
Class String::GetClass() { return Class(util::string::GetClass()); }
29+
FIREBASE_DEV_ASSERT(utf8_string == nullptr);
30+
Local<String> utf8(env.get(), env.get()->NewStringUTF("UTF-8"));
31+
if (!env.ok()) return;
1232

13-
std::string String::ToString(Env& env) const {
14-
size_t len = env.GetStringUtfLength(*this);
15-
return env.GetStringUtfRegion(*this, 0, len);
33+
utf8_string = Global<String>(utf8).release();
1634
}
1735

36+
void String::Terminate(Env& env) {
37+
if (utf8_string) {
38+
env.get()->DeleteGlobalRef(utf8_string);
39+
utf8_string = nullptr;
40+
}
41+
}
42+
43+
Class String::GetClass() { return Class(string_class); }
44+
45+
String String::GetUtf8() { return String(utf8_string); }
46+
47+
Local<String> String::Create(Env& env, const Array<uint8_t>& bytes,
48+
const String& encoding) {
49+
return env.New(kNewFromBytes, bytes, encoding);
50+
}
51+
52+
Local<Array<uint8_t>> String::GetBytes(Env& env, const String& encoding) const {
53+
return env.Call(*this, kGetBytes, encoding);
54+
}
55+
56+
std::string String::ToString(Env& env) const { return env.ToStringUtf(*this); }
57+
1858
} // namespace jni
1959
} // namespace firestore
2060
} // namespace firebase

firestore/src/jni/string.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,34 @@ class String : public Object {
2222
String() = default;
2323
explicit String(jstring string) : Object(string) {}
2424

25+
/**
26+
* Creates a new Java String from the given bytes, using the given encoding.
27+
* This matches the behavior of the Java `String(byte[], String)` constructor.
28+
*
29+
* @param bytes A Java array of encoded bytes.
30+
* @param encoding A Java string naming the encoding of the bytes.
31+
*/
32+
static Local<String> Create(Env& env, const Array<uint8_t>& bytes,
33+
const String& encoding);
34+
2535
jstring get() const override { return static_cast<jstring>(object_); }
2636

37+
static void Initialize(Env& env, Loader& loader);
38+
static void Terminate(Env& env);
39+
2740
static Class GetClass();
2841

29-
/** Converts this Java String to a C++ string. */
42+
/** Returns a Java String representing "UTF-8". */
43+
static String GetUtf8();
44+
45+
Local<Array<uint8_t>> GetBytes(Env& env, const String& encoding) const;
46+
47+
/**
48+
* Converts this Java String to a C++ string encoded in UTF-8.
49+
*
50+
* Note that this string is encoded in standard UTF-8, and *not* in the
51+
* modified UTF-8 customarily used in the JNI API.
52+
*/
3053
std::string ToString(Env& env) const;
3154
};
3255

0 commit comments

Comments
 (0)