Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions webrtc-media/src/main/cpp/include/media/JniBoundary.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ namespace ffmpeg
// C++ heap, so that it can also report that an allocation failed.
void ThrowJavaException(JNIEnv * env, const char * class_name, const char * message) noexcept;

// The message is UTF-8, as is everything FFmpeg and this library write.
void ThrowIOException(JNIEnv * env, const std::string & message) noexcept;

// A Java string of the given UTF-8, in which what is not UTF-8 becomes
// U+FFFD, as it does in Java. Null if it could not be made, and a Java
// exception may be pending then.
jstring NewJavaString(JNIEnv * env, const std::string & utf8) noexcept;

// Turns the C++ exception being handled into a Java one: a failed
// allocation into an OutOfMemoryError, anything else into a
// RuntimeException. Only to be called from a catch block.
Expand Down Expand Up @@ -102,8 +108,8 @@ namespace ffmpeg
T ref_;
};

// The modified UTF-8 of a Java string, released when this goes out of
// scope, also when a copy of it fails on the way out.
// The characters of a Java string, released when this goes out of scope,
// also when a copy of them fails on the way out.
class JavaStringChars
{
public:
Expand All @@ -115,13 +121,17 @@ namespace ffmpeg
JavaStringChars(const JavaStringChars &) = delete;
JavaStringChars & operator=(const JavaStringChars &) = delete;

const char * get() const noexcept
const jchar * get() const noexcept
{
return chars_;
}

// A copy that outlives this object. Throws std::bad_alloc.
std::string str() const;
// The string as UTF-8, which FFmpeg takes, and not as the modified
// UTF-8 of JNI, which writes the characters outside the Basic
// Multilingual Plane as surrogates and U+0000 as two bytes. A
// surrogate without its pair becomes '?', as it does in Java.
// Throws std::bad_alloc.
std::string utf8() const;

// How many strings are held right now, for the tests to see that
// none is left behind.
Expand All @@ -130,7 +140,8 @@ namespace ffmpeg
private:
JNIEnv * env_;
jstring string_;
const char * chars_;
const jchar * chars_;
jsize length_;
};
}

Expand Down
4 changes: 2 additions & 2 deletions webrtc-media/src/main/cpp/src/JNI_AllocationFault.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_player_AllocationFault_throw
case 0:
throw std::bad_alloc();
case 1:
throw std::runtime_error(text.get());
throw std::runtime_error(text.utf8());
case 2:
// A JNI call that failed, and a C++ exception after it.
env->FindClass("dev/onvoid/webrtc/media/player/NoSuchClass");

throw std::runtime_error(text.get());
throw std::runtime_error(text.utf8());
default:
throw kind;
}
Expand Down
9 changes: 8 additions & 1 deletion webrtc-media/src/main/cpp/src/JNI_MediaReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,14 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_player_MediaReader_open
return 0;
}

url = chars.str();
url = chars.utf8();
}

// FFmpeg would read the source only up to it, and open another one.
if (url.find('\0') != std::string::npos) {
ffmpeg::ThrowIOException(env, "Source must not contain a null character");

return 0;
}

auto reader = std::make_unique<ffmpeg::MediaReader>(static_cast<int64_t>(timeoutUs));
Expand Down
2 changes: 1 addition & 1 deletion webrtc-media/src/main/cpp/src/JNI_MediaRecorder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ JNIEXPORT jlong JNICALL Java_dev_onvoid_webrtc_media_recorder_MediaRecorder_crea
return 0;
}

path = chars.str();
path = chars.utf8();
}

auto recorder = std::make_unique<ffmpeg::MediaRecorder>(api, path);
Expand Down
3 changes: 2 additions & 1 deletion webrtc-media/src/main/cpp/src/media/JavaPlayerObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#include "media/JavaPlayerObserver.h"
#include "media/JniBoundary.h"

namespace ffmpeg
{
Expand Down Expand Up @@ -109,7 +110,7 @@ namespace ffmpeg
JNIEnv * env = Attach(&attached);

if (CanCallJava(env)) {
jstring text = env->NewStringUTF(message.c_str());
jstring text = NewJavaString(env, message);

if (text != nullptr) {
env->CallVoidMethod(player_, on_error_, text);
Expand Down
3 changes: 2 additions & 1 deletion webrtc-media/src/main/cpp/src/media/JavaRecorderObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#include "media/JavaRecorderObserver.h"
#include "media/JniBoundary.h"

namespace ffmpeg
{
Expand Down Expand Up @@ -120,7 +121,7 @@ namespace ffmpeg
JNIEnv * env = Attach(&attached);

if (CanCallJava(env)) {
jstring text = env->NewStringUTF(message.c_str());
jstring text = NewJavaString(env, message);

if (text != nullptr) {
env->CallVoidMethod(recorder_, method, text);
Expand Down
149 changes: 143 additions & 6 deletions webrtc-media/src/main/cpp/src/media/JniBoundary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,108 @@ namespace

buffer[length] = '\0';
}

// Writes a code point as UTF-8, or as the modified UTF-8 of JNI, which
// writes U+0000 as two bytes and one beyond U+FFFF as its two UTF-16
// surrogates, three bytes each.
void AppendCodePoint(std::string & out, char32_t c, bool modified)
{
if (modified && c > 0xFFFF) {
c -= 0x10000;

AppendCodePoint(out, 0xD800 + (c >> 10), true);
AppendCodePoint(out, 0xDC00 + (c & 0x3FF), true);
}
else if (c < 0x80 && !(modified && c == 0)) {
out += static_cast<char>(c);
}
else if (c < 0x800) {
out += static_cast<char>(0xC0 | (c >> 6));
out += static_cast<char>(0x80 | (c & 0x3F));
}
else if (c < 0x10000) {
out += static_cast<char>(0xE0 | (c >> 12));
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
out += static_cast<char>(0x80 | (c & 0x3F));
}
else {
out += static_cast<char>(0xF0 | (c >> 18));
out += static_cast<char>(0x80 | ((c >> 12) & 0x3F));
out += static_cast<char>(0x80 | ((c >> 6) & 0x3F));
out += static_cast<char>(0x80 | (c & 0x3F));
}
}

// Reads the code point at `pos` and moves past it. What is not UTF-8 is
// read as U+FFFD, one byte at a time.
char32_t NextCodePoint(const std::string & text, size_t & pos) noexcept
{
const unsigned char lead = static_cast<unsigned char>(text[pos++]);

if (lead < 0x80) {
return lead;
}

size_t count;
char32_t c;
char32_t min;

if ((lead & 0xE0) == 0xC0) {
count = 1;
c = lead & 0x1F;
min = 0x80;
}
else if ((lead & 0xF0) == 0xE0) {
count = 2;
c = lead & 0x0F;
min = 0x800;
}
else if ((lead & 0xF8) == 0xF0) {
count = 3;
c = lead & 0x07;
min = 0x10000;
}
else {
return 0xFFFD;
}

if (text.size() - pos < count) {
return 0xFFFD;
}

for (size_t i = 0; i < count; i++) {
const unsigned char next = static_cast<unsigned char>(text[pos + i]);

if ((next & 0xC0) != 0x80) {
return 0xFFFD;
}

c = (c << 6) | (next & 0x3F);
}

// Overlong, a surrogate, or beyond Unicode.
if (c < min || (c >= 0xD800 && c <= 0xDFFF) || c > 0x10FFFF) {
return 0xFFFD;
}

pos += count;

return c;
}

std::string ToModifiedUtf8(const std::string & utf8)
{
std::string out;
size_t pos = 0;

out.reserve(utf8.size());

while (pos < utf8.size()) {
AppendCodePoint(out, NextCodePoint(utf8, pos), true);
}

return out;
}
}

namespace ffmpeg
Expand All @@ -64,7 +166,22 @@ namespace ffmpeg

void ThrowIOException(JNIEnv * env, const std::string & message) noexcept
{
ThrowJavaException(env, "java/io/IOException", message.c_str());
try {
ThrowJavaException(env, "java/io/IOException", ToModifiedUtf8(message).c_str());
}
catch (const std::bad_alloc &) {
ThrowJavaException(env, "java/lang/OutOfMemoryError", "Native memory allocation failed");
}
}

jstring NewJavaString(JNIEnv * env, const std::string & utf8) noexcept
{
try {
return env->NewStringUTF(ToModifiedUtf8(utf8).c_str());
}
catch (const std::bad_alloc &) {
return nullptr;
}
}

void ThrowCurrentException(JNIEnv * env) noexcept
Expand All @@ -90,34 +207,54 @@ namespace ffmpeg
JavaStringChars::JavaStringChars(JNIEnv * env, jstring string) noexcept :
env_(env),
string_(string),
chars_(nullptr)
chars_(nullptr),
length_(0)
{
if (string == nullptr) {
ThrowJavaException(env, "java/lang/NullPointerException", "String is null");
return;
}

// Null when the VM is out of memory, and it has thrown then.
chars_ = env->GetStringUTFChars(string, nullptr);
chars_ = env->GetStringChars(string, nullptr);

if (chars_ != nullptr) {
length_ = env->GetStringLength(string);
held_strings.fetch_add(1);
}
}

JavaStringChars::~JavaStringChars()
{
if (chars_ != nullptr) {
env_->ReleaseStringUTFChars(string_, chars_);
env_->ReleaseStringChars(string_, chars_);
held_strings.fetch_sub(1);
}
}

std::string JavaStringChars::str() const
std::string JavaStringChars::utf8() const
{
AllocationFault::CheckStringCopy();

return std::string(chars_);
std::string out;

out.reserve(static_cast<size_t>(length_));

for (jsize i = 0; i < length_; i++) {
char32_t c = chars_[i];

if (c >= 0xD800 && c <= 0xDBFF && i + 1 < length_
&& chars_[i + 1] >= 0xDC00 && chars_[i + 1] <= 0xDFFF) {
c = 0x10000 + ((c - 0xD800) << 10) + (chars_[++i] - 0xDC00);
}
else if (c >= 0xD800 && c <= 0xDFFF) {
c = '?';
}

AppendCodePoint(out, c, false);
}

return out;
}

int JavaStringChars::Held() noexcept
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
Expand All @@ -31,6 +32,7 @@
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
* Tests reading a media source. The asset is three seconds of VP8 and Opus in
Expand Down Expand Up @@ -128,6 +130,41 @@ void missingSourceFails() {
assertTrue(e.getMessage().contains("no-such-file.webm"), e.getMessage());
}

@Test
void opensPathBeyondBasicPlane(@TempDir Path dir) throws Exception {
// FFmpeg takes UTF-8, which is not the modified UTF-8 of JNI for
// characters beyond the Basic Multilingual Plane.
Path file = UnicodeFileName.resolve(dir, "webm");

Files.copy(asset(), file);

try (MediaReader reader = new MediaReader(file)) {
assertTrue(reader.getInfo().hasVideo());
}
try (MediaReader reader = new MediaReader(file.toString())) {
assertTrue(reader.getInfo().hasAudio());
}
}

@Test
void missingSourceBeyondBasicPlaneFails(@TempDir Path dir) {
Path file = UnicodeFileName.resolve(dir, "webm");

IOException e = assertThrows(IOException.class, () -> new MediaReader(file));

// The name comes back to Java as it went in.
assertTrue(e.getMessage().contains(UnicodeFileName.NAME), e.getMessage());
}

@Test
void rejectsSourceWithNullCharacter() throws Exception {
// Read up to the null character, the source would be the asset.
String source = asset().toAbsolutePath() + "\u0000.txt";

assertThrows(IOException.class, () -> new MediaReader(source));
assertEquals(0, AllocationFault.heldStrings());
}

@Test
void sourceCopyFails() throws Exception {
// The native copy of the source fails as an exhausted heap does: the
Expand Down
Loading
Loading