hphp/runtime/base/variable-serializer.cpp (2,175 lines of code) (raw):
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/base/variable-serializer.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/array-provenance.h"
#include "hphp/runtime/base/backtrace.h"
#include "hphp/runtime/base/collections.h"
#include "hphp/runtime/base/comparisons.h"
#include "hphp/runtime/base/execution-context.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/tv-refcount.h"
#include "hphp/runtime/base/type-variant.h"
#include "hphp/runtime/base/utf8-decode.h"
#include "hphp/runtime/base/vanilla-dict-defs.h"
#include "hphp/runtime/base/vanilla-dict.h"
#include "hphp/runtime/base/vanilla-vec-defs.h"
#include "hphp/runtime/base/zend-functions.h"
#include "hphp/runtime/base/zend-printf.h"
#include "hphp/runtime/base/zend-string.h"
#include "hphp/runtime/ext/collections/ext_collections.h"
#include "hphp/runtime/ext/json/JSON_parser.h"
#include "hphp/runtime/ext/json/ext_json.h"
#include "hphp/runtime/ext/std/ext_std_closure.h"
#include "hphp/runtime/vm/native-data.h"
#include "hphp/runtime/vm/class-meth-data-ref.h"
#include "hphp/util/exception.h"
#include "hphp/util/rds-local.h"
#include <cmath>
namespace HPHP {
///////////////////////////////////////////////////////////////////////////////
const StaticString
s_serializedNativeDataKey("\0native"),
s_JsonSerializable("JsonSerializable"),
s_jsonSerialize("jsonSerialize"),
s_serialize("serialize"),
s_zero("\0"),
s_protected_prefix("\0*\0"),
s_PHP_DebugDisplay("__PHP_DebugDisplay"),
s_PHP_Incomplete_Class("__PHP_Incomplete_Class"),
s_PHP_Incomplete_Class_Name("__PHP_Incomplete_Class_Name"),
s_debugInfo("__debugInfo"),
s_message("message");
[[noreturn]] NEVER_INLINE
static void throwNestingException() {
throw ExtendedException("Nesting level too deep - recursive dependency?");
}
///////////////////////////////////////////////////////////////////////////////
VariableSerializer::SavedRefMap::~SavedRefMap() {
for (auto& i : m_mapping) {
tvDecRefGen(const_cast<TypedValue*>(&i.first));
}
}
VariableSerializer::~VariableSerializer() {
}
VariableSerializer::VariableSerializer(Type type, int option /* = 0 */,
int maxRecur /* = 3 */)
: m_type(type)
, m_option(option)
, m_keepDVArrays{type != Type::Serialize}
, m_maxCount(maxRecur)
{
if (type == Type::DebuggerSerialize) {
m_maxLevelDebugger = g_context->debuggerSettings.printLevel;
}
}
VariableSerializer::ArrayKind
VariableSerializer::getKind(const ArrayData* arr) const {
if (UNLIKELY(m_forcePHPArrays)) {
return VariableSerializer::ArrayKind::PHP;
} else if (UNLIKELY(m_forceHackArrays)) {
if (arr->isDictType()) {
return VariableSerializer::ArrayKind::Dict;
} else if (arr->isVecType()) {
return VariableSerializer::ArrayKind::Vec;
}
assertx(arr->isKeysetType());
return VariableSerializer::ArrayKind::Keyset;
}
auto const respectsLegacyBit = [&] {
switch (getType()) {
case Type::PrintR:
case Type::VarDump:
case Type::VarExport:
case Type::Serialize:
case Type::JSON:
case Type::DebuggerDump:
case Type::DebuggerSerialize:
return true;
case Type::Internal:
case Type::DebugDump:
case Type::PHPOutput:
case Type::APCSerialize:
return false;
}
always_assert(false);
}();
auto const serializesLegacyBit =
getType() == Type::Internal || getType() == Type::APCSerialize ||
(getType() == Type::Serialize && m_serializeProvenanceAndLegacy);
if (serializesLegacyBit && arr->isLegacyArray()) {
assertx(!arr->isKeysetType());
if (m_keepDVArrays && arr->isVecType()) {
return VariableSerializer::ArrayKind::MarkedVArray;
}
return VariableSerializer::ArrayKind::MarkedDArray;
}
if (respectsLegacyBit && arr->isLegacyArray()) {
assertx(!arr->isKeysetType());
if (m_keepDVArrays) {
return arr->isVecType() ? VariableSerializer::ArrayKind::VArray
: VariableSerializer::ArrayKind::DArray;
}
return VariableSerializer::ArrayKind::PHP;
}
if (arr->isDictType()) return VariableSerializer::ArrayKind::Dict;
if (arr->isVecType()) return VariableSerializer::ArrayKind::Vec;
assertx(arr->isKeysetType());
return VariableSerializer::ArrayKind::Keyset;
}
void VariableSerializer::pushObjectInfo(const String& objClass, char objCode) {
assertx(objCode == 'O' || objCode == 'V' || objCode == 'K');
m_objectInfos.emplace_back(
ObjectInfo { m_objClass, m_objCode, m_rsrcName, m_rsrcId }
);
m_objClass = objClass;
m_objCode = objCode;
m_rsrcName.reset();
m_rsrcId = 0;
}
void VariableSerializer::pushResourceInfo(const String& rsrcName, int rsrcId) {
m_objectInfos.emplace_back(
ObjectInfo { m_objClass, m_objCode, m_rsrcName, m_rsrcId }
);
m_objClass.reset();
m_objCode = 0;
m_rsrcName = rsrcName;
m_rsrcId = rsrcId;
}
void VariableSerializer::popObjectInfo() {
ObjectInfo &info = m_objectInfos.back();
m_objClass = info.objClass;
m_objCode = info.objCode;
m_rsrcName = info.rsrcName;
m_rsrcId = info.rsrcId;
m_objectInfos.pop_back();
}
RDS_LOCAL(VariableSerializer::SerializationLimitWrapper,
VariableSerializer::serializationSizeLimit);
void VariableSerializer::popResourceInfo() {
popObjectInfo();
}
String VariableSerializer::serialize(const_variant_ref v, bool ret,
bool keepCount /* = false */) {
StringBuffer buf;
m_buf = &buf;
if (ret) {
buf.setOutputLimit(serializationSizeLimit->value);
} else {
buf.setOutputLimit(StringData::MaxSize);
}
m_valueCount = keepCount ? m_valueCount + 1 : 1;
write(v);
if (ret) {
return m_buf->detach();
} else {
String str = m_buf->detach();
g_context->write(str);
}
return String();
}
String VariableSerializer::serializeValue(const Variant& v, bool limit) {
StringBuffer buf;
m_buf = &buf;
if (limit) {
buf.setOutputLimit(serializationSizeLimit->value);
}
m_valueCount = 1;
write(v);
return m_buf->detach();
}
String VariableSerializer::serializeWithLimit(const Variant& v, int limit) {
if (m_type == Type::Serialize || m_type == Type::Internal ||
m_type == Type::JSON || m_type == Type::APCSerialize ||
m_type == Type::DebuggerSerialize) {
assertx(false);
return String();
}
StringBuffer buf;
m_buf = &buf;
if (serializationSizeLimit->value > 0 &&
(limit <= 0 || limit > serializationSizeLimit->value)) {
limit = serializationSizeLimit->value;
}
buf.setOutputLimit(limit);
//Does not need m_valueCount, which is only useful with the unsupported types
try {
write(v);
} catch (StringBufferLimitException& e) {
return e.m_result;
}
return m_buf->detach();
}
///////////////////////////////////////////////////////////////////////////////
void VariableSerializer::write(bool v) {
switch (m_type) {
case Type::PrintR:
if (v) m_buf->append(1);
break;
case Type::VarExport:
case Type::PHPOutput:
case Type::JSON:
case Type::DebuggerDump:
m_buf->append(v ? "true" : "false");
break;
case Type::VarDump:
case Type::DebugDump:
indent();
m_buf->append(v ? "bool(true)" : "bool(false)");
writeRefCount();
m_buf->append('\n');
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append(v ? "b:1;" : "b:0;");
break;
default:
assertx(false);
break;
}
}
void VariableSerializer::write(int64_t v) {
switch (m_type) {
case Type::PrintR:
case Type::VarExport:
case Type::PHPOutput:
case Type::JSON:
case Type::DebuggerDump:
m_buf->append(v);
break;
case Type::VarDump:
indent();
m_buf->append("int(");
m_buf->append(v);
m_buf->append(")\n");
break;
case Type::DebugDump:
indent();
m_buf->append("long(");
m_buf->append(v);
m_buf->append(')');
writeRefCount();
m_buf->append('\n');
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append("i:");
m_buf->append(v);
m_buf->append(';');
break;
default:
assertx(false);
break;
}
}
void VariableSerializer::write(double v) {
auto const precision = 14;
auto const serde_precision = 17;
switch (m_type) {
case Type::JSON:
if (!std::isinf(v) && !std::isnan(v)) {
char *buf;
vspprintf(&buf, 0, "%.*k", precision, v);
m_buf->append(buf);
if (m_option & k_JSON_PRESERVE_ZERO_FRACTION
&& strchr(buf, '.') == nullptr) {
m_buf->append(".0");
}
free(buf);
} else {
json_set_last_error_code(json_error_codes::JSON_ERROR_INF_OR_NAN);
m_buf->append('0');
}
break;
case Type::VarExport:
case Type::PHPOutput:
case Type::PrintR:
case Type::DebuggerDump:
{
char *buf;
bool isExport = m_type == Type::VarExport || m_type == Type::PHPOutput;
vspprintf(&buf, 0, isExport ? "%.*H" : "%.*G", precision, v);
m_buf->append(buf);
// In PHPOutput mode, we always want doubles to parse as
// doubles, so make sure there's a decimal point.
if (m_type == Type::PHPOutput && strpbrk(buf, ".E") == nullptr) {
m_buf->append(".0");
}
free(buf);
}
break;
case Type::VarDump:
case Type::DebugDump:
{
char *buf;
vspprintf(&buf, 0, "float(%.*G)", precision, v);
indent();
m_buf->append(buf);
free(buf);
writeRefCount();
m_buf->append('\n');
}
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append("d:");
if (std::isnan(v)) {
m_buf->append("NAN");
} else if (std::isinf(v)) {
if (v < 0) m_buf->append('-');
m_buf->append("INF");
} else {
char *buf;
vspprintf(&buf, 0, "%.*H", serde_precision, v);
m_buf->append(buf);
free(buf);
}
m_buf->append(';');
break;
default:
assertx(false);
break;
}
}
uint16_t reverse16(uint16_t us) {
return
((us & 0xf) << 12) | (((us >> 4) & 0xf) << 8) |
(((us >> 8) & 0xf) << 4) | ((us >> 12) & 0xf);
}
// Potentially need to escape all control characters (< 32) and also "\/<>&'@%
static const bool jsonNoEscape[128] = {
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false,
true, true, false, true, true, false, false, false,
true, true, true, true, true, true, true, false,
true, true, true, true, true, true, true, true,
true, true, true, true, false, true, false, true,
false, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true,
true, true, true, true, false, true, true, true,
true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true,
true, true, true, true, true, true, true, true,
};
static void appendJsonEscape(StringBuffer& sb,
const char *s,
int len,
int options) {
if (len == 0) {
sb.append("\"\"", 2);
return;
}
static const char digits[] = "0123456789abcdef";
auto const start = sb.size();
sb.append('"');
// Do a fast path for ASCII characters that don't need escaping
int pos = 0;
do {
int c = s[pos];
if (UNLIKELY((unsigned char)c >= 128 || !jsonNoEscape[c])) {
goto utf8_decode;
}
sb.append((char)c);
pos++;
} while (pos < len);
sb.append('"');
return;
utf8_decode:
UTF8To16Decoder decoder(s + pos, len - pos, options & k_JSON_FB_LOOSE);
for (;;) {
int c = options & k_JSON_UNESCAPED_UNICODE ? decoder.decodeAsUTF8()
: decoder.decode();
if (c == UTF8_END) {
sb.append('"');
break;
}
if (c == UTF8_ERROR) {
json_set_last_error_code(json_error_codes::JSON_ERROR_UTF8);
// discard the part that has been already decoded.
sb.resize(start);
sb.append("null", 4);
break;
}
assertx(c >= 0);
unsigned short us = (unsigned short)c;
switch (us) {
case '"':
if (options & k_JSON_HEX_QUOT) {
sb.append("\\u0022", 6);
} else {
sb.append("\\\"", 2);
}
break;
case '\\': sb.append("\\\\", 2); break;
case '/':
if (options & k_JSON_UNESCAPED_SLASHES) {
sb.append('/');
} else {
sb.append("\\/", 2);
}
break;
case '\b': sb.append("\\b", 2); break;
case '\f': sb.append("\\f", 2); break;
case '\n': sb.append("\\n", 2); break;
case '\r': sb.append("\\r", 2); break;
case '\t': sb.append("\\t", 2); break;
case '<':
if (options & k_JSON_HEX_TAG || options & k_JSON_FB_EXTRA_ESCAPES) {
sb.append("\\u003C", 6);
} else {
sb.append('<');
}
break;
case '>':
if (options & k_JSON_HEX_TAG) {
sb.append("\\u003E", 6);
} else {
sb.append('>');
}
break;
case '&':
if (options & k_JSON_HEX_AMP) {
sb.append("\\u0026", 6);
} else {
sb.append('&');
}
break;
case '\'':
if (options & k_JSON_HEX_APOS) {
sb.append("\\u0027", 6);
} else {
sb.append('\'');
}
break;
case '@':
if (options & k_JSON_FB_EXTRA_ESCAPES) {
sb.append("\\u0040", 6);
} else {
sb.append('@');
}
break;
case '%':
if (options & k_JSON_FB_EXTRA_ESCAPES) {
sb.append("\\u0025", 6);
} else {
sb.append('%');
}
break;
default:
if (us >= ' ' &&
((options & k_JSON_UNESCAPED_UNICODE) || (us & 127) == us)) {
sb.append((char)us);
} else {
sb.append("\\u", 2);
us = reverse16(us);
sb.append(digits[us & ((1 << 4) - 1)]); us >>= 4;
sb.append(digits[us & ((1 << 4) - 1)]); us >>= 4;
sb.append(digits[us & ((1 << 4) - 1)]); us >>= 4;
sb.append(digits[us & ((1 << 4) - 1)]);
}
break;
}
}
}
void VariableSerializer::write(const char *v, int len /* = -1 */,
bool isArrayKey /* = false */,
bool noQuotes /* = false */) {
if (v == nullptr) v = "";
if (len < 0) len = strlen(v);
switch (m_type) {
case Type::PrintR: {
m_buf->append(v, len);
break;
}
case Type::VarExport: {
m_buf->append('\'');
const char *p = v;
for (int i = 0; i < len; i++, p++) {
const char c = *p;
// adapted from Zend php_var_export and php_addcslashes
if (c == '\0') {
m_buf->append("' . \"\\0\" . '");
continue;
} else if (c == '\'' || c == '\\') {
m_buf->append('\\');
}
m_buf->append(c);
}
m_buf->append('\'');
break;
}
case Type::VarDump:
case Type::DebugDump: {
indent();
m_buf->append("string(");
m_buf->append(len);
m_buf->append(") \"");
m_buf->append(v, len);
m_buf->append('"');
writeRefCount();
m_buf->append('\n');
break;
}
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append("s:");
m_buf->append(len);
m_buf->append(":\"");
m_buf->append(v, len);
m_buf->append("\";");
break;
case Type::JSON: {
if ((m_option & k_JSON_NUMERIC_CHECK) && !isArrayKey) {
int64_t lval; double dval;
auto dt = is_numeric_string(v, len, &lval, &dval, 0);
if (isIntType(dt)) {
write(lval);
return;
} else if (isDoubleType(dt)) {
write(dval);
return;
}
}
appendJsonEscape(*m_buf, v, len, m_option);
break;
}
case Type::DebuggerDump:
case Type::PHPOutput: {
if (!noQuotes)
m_buf->append('"');
for (int i = 0; i < len; ++i) {
const unsigned char c = v[i];
switch (c) {
case '\n': m_buf->append("\\n"); break;
case '\r': m_buf->append("\\r"); break;
case '\t': m_buf->append("\\t"); break;
case '\\': m_buf->append("\\\\"); break;
case '$': m_buf->append("\\$"); break;
case '"': m_buf->append("\\\""); break;
default: {
if (c >= ' ' && c <= '~') {
// The range [' ', '~'] contains only printable characters
// and we've already handled special cases above
m_buf->append(c);
} else {
char buf[5];
snprintf(buf, sizeof(buf), "\\%03o", c);
m_buf->append(buf);
}
}
}
}
if (!noQuotes)
m_buf->append('"');
break;
}
default:
assertx(false);
break;
}
}
void VariableSerializer::write(const String& v) {
if (m_type == Type::APCSerialize && !v.isNull() && v.get()->isStatic()) {
union {
char buf[8];
StringData *sd;
} u;
u.sd = v.get();
m_buf->append("S:");
m_buf->append(u.buf, 8);
m_buf->append(';');
} else {
serializeString(v);
}
}
const StaticString
s_invalidMethCallerAPC("Cannot store meth_caller in APC"),
s_invalidMethCallerSerde("Cannot serialize meth_caller"),
s_disallowedObjectSerde("Cannot serialize object due to options");
void VariableSerializer::write(const Object& v) {
if (!v.isNull() && m_type == Type::JSON) {
if (RO::EvalForbidMethCallerHelperSerialize &&
v.get()->getVMClass() == SystemLib::s_MethCallerHelperClass) {
if (RO::EvalForbidMethCallerHelperSerialize == 1) {
raise_warning("Serializing MethCallerHelper");
} else {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerSerde.get()}
);
}
}
// m_disallowObjects not relevent in JSON path bc it's only
//setable via serialize_with_options
if (v.instanceof(s_JsonSerializable)) {
assertx(!v->isCollection());
auto const providedCoeffects =
m_pure ? RuntimeCoeffects::pure() : RuntimeCoeffects::defaults();
Variant ret = v->o_invoke_few_args(s_jsonSerialize, providedCoeffects, 0);
// for non objects or when $this is not returned
if (!ret.isObject() || ret.getObjectData() != v.get()) {
if (ret.isArray() || ret.isObject()) {
preventOverflow(v, [&ret, this]() {
write(ret);
});
} else {
// Don't need to check for overflows if ret is of primitive type
// because the depth does not change.
write(ret);
}
return;
}
}
preventOverflow(v, [&v, this]() {
if (v->isCollection()) {
serializeCollection(v.get());
} else if (v->instanceof(c_Closure::classof())) {
m_buf->append("null");
json_set_last_error_code(
json_error_codes::JSON_ERROR_UNSUPPORTED_TYPE);
return;
} else {
auto props = v->toArray(true, m_ignoreLateInit);
pushObjectInfo(v->getClassName(), 'O');
serializeObjProps(props);
popObjectInfo();
}
});
} else {
serializeObject(v);
}
}
void VariableSerializer::preventOverflow(const Object& v,
const std::function<void()>& func) {
TypedValue tv = make_tv<KindOfObject>(const_cast<ObjectData*>(v.get()));
if (incNestedLevel(&tv)) {
writeOverflow(&tv);
} else {
func();
}
decNestedLevel(&tv);
}
void VariableSerializer::write(const_variant_ref v, bool isArrayKey) {
if (m_type == Type::DebugDump) {
setRefCount(v.getRefCount());
}
if (!isArrayKey && v.isObject()) {
write(v.toObject());
return;
}
serializeVariant(v.rval(), isArrayKey);
}
void VariableSerializer::writeNull() {
switch (m_type) {
case Type::PrintR:
// do nothing
break;
case Type::VarExport:
case Type::PHPOutput:
m_buf->append("NULL");
break;
case Type::VarDump:
case Type::DebugDump:
indent();
m_buf->append("NULL");
writeRefCount();
m_buf->append('\n');
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append("N;");
break;
case Type::JSON:
case Type::DebuggerDump:
m_buf->append("null");
break;
default:
assertx(false);
break;
}
}
void VariableSerializer::writeOverflow(tv_rval tv) {
switch (m_type) {
case Type::PrintR:
if (!m_objClass.empty()) {
m_buf->append(m_objClass);
m_buf->append(" Object\n");
} else {
m_buf->append("Array\n");
}
m_buf->append(" *RECURSION*");
break;
case Type::VarExport:
case Type::PHPOutput:
throwNestingException();
case Type::VarDump:
case Type::DebugDump:
case Type::DebuggerDump:
indent();
m_buf->append("*RECURSION*\n");
break;
case Type::DebuggerSerialize:
if (m_maxLevelDebugger > 0 && m_levelDebugger > m_maxLevelDebugger) {
// Not recursion, just cut short of print
m_buf->append("s:12:\"...(omitted)\";", 20);
break;
}
// fall through
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
{
int optId = m_refs[tv].m_id;
assertx(optId != NO_ID);
bool isObject = tvIsResource(tv) || tvIsObject(tv);
if (isObject) {
m_buf->append("r:");
m_buf->append(optId);
m_buf->append(';');
} else {
m_buf->append("N;");
}
}
break;
case Type::JSON:
json_set_last_error_code(json_error_codes::JSON_ERROR_RECURSION);
m_buf->append("null");
break;
default:
assertx(false);
break;
}
}
void VariableSerializer::writeRefCount() {
if (m_type != Type::DebugDump) return;
if (m_refCount >= 0) {
m_buf->append(" refcount(");
m_buf->append(m_refCount);
m_buf->append(')');
} else if (m_refCount == StaticValue) {
m_buf->append(" static");
} else {
m_buf->append(" uncounted");
}
m_refCount = OneReference;
}
void VariableSerializer::writeArrayHeader(int size, bool isVectorData,
VariableSerializer::ArrayKind kind) {
m_arrayInfos.push_back(ArrayInfo());
ArrayInfo &info = m_arrayInfos.back();
info.first_element = true;
info.indent_delta = 0;
info.size = size;
switch (m_type) {
case Type::DebuggerDump:
case Type::PrintR:
if (!m_rsrcName.empty()) {
m_buf->append("Resource id #");
m_buf->append(m_rsrcId);
if (m_type == Type::DebuggerDump) {
m_buf->append(" of type ");
m_buf->append(m_rsrcName);
}
break;
} else if (!m_objClass.empty()) {
m_buf->append(m_objClass);
m_buf->append(" Object\n");
} else {
switch (kind) {
case ArrayKind::Dict:
m_buf->append("Dict\n");
break;
case ArrayKind::Vec:
m_buf->append("Vec\n");
break;
case ArrayKind::Keyset:
m_buf->append("Keyset\n");
break;
case ArrayKind::PHP:
case ArrayKind::VArray:
case ArrayKind::DArray:
case ArrayKind::MarkedVArray:
case ArrayKind::MarkedDArray:
m_buf->append("Array\n");
break;
}
}
if (m_indent > 0) {
m_indent += 4;
indent();
}
m_buf->append("(\n");
m_indent += (info.indent_delta = 4);
break;
case Type::VarExport:
case Type::PHPOutput:
if (m_indent > 0 && m_rsrcName.empty() && m_keyPrinted) {
m_buf->append('\n');
indent();
}
if (!m_objClass.empty()) {
m_buf->append(m_objClass);
if (m_objCode == 'O') {
m_buf->append("::__set_state(darray[\n");
} else {
assertx(m_objCode == 'V' || m_objCode == 'K');
m_buf->append(" {\n");
}
} else if (!m_rsrcName.empty()) {
m_buf->append("NULL");
} else {
switch (kind) {
case ArrayKind::Dict:
m_buf->append("dict [\n");
break;
case ArrayKind::Vec:
m_buf->append("vec [\n");
break;
case ArrayKind::Keyset:
m_buf->append("keyset [\n");
break;
case ArrayKind::PHP:
m_buf->append("array (\n");
break;
case ArrayKind::VArray: {
auto const dvarray = RO::EvalHackArrDVArrVarExport ||
m_type == Type::PHPOutput;
m_buf->append(dvarray ? "varray [\n" : "array (\n");
break;
}
case ArrayKind::DArray: {
auto const dvarray = RO::EvalHackArrDVArrVarExport ||
m_type == Type::PHPOutput;
m_buf->append(dvarray ? "darray [\n" : "array (\n");
break;
}
case ArrayKind::MarkedVArray:
case ArrayKind::MarkedDArray:
always_assert(0);
}
}
m_indent += (info.indent_delta = 2);
break;
case Type::VarDump:
case Type::DebugDump:
indent();
if (!m_rsrcName.empty()) {
m_buf->append("resource(");
m_buf->append(m_rsrcId);
m_buf->append(") of type (");
m_buf->append(m_rsrcName);
m_buf->append(")\n");
break;
} else if (!m_objClass.empty()) {
m_buf->append("object(");
m_buf->append(m_objClass);
m_buf->append(") ");
} else {
auto const header = [&]() {
switch (kind) {
case ArrayKind::Dict:
return "dict";
case ArrayKind::Vec:
return "vec";
case ArrayKind::Keyset:
return "keyset";
case ArrayKind::PHP:
return "array";
case ArrayKind::VArray:
case ArrayKind::MarkedVArray:
return "varray";
case ArrayKind::DArray:
case ArrayKind::MarkedDArray:
return "darray";
}
not_reached();
}();
m_buf->append(header);
}
m_buf->append('(');
m_buf->append(size);
m_buf->append(')');
// ...so to strictly follow PHP's output
if (m_type == Type::VarDump) {
m_buf->append(' ');
} else {
writeRefCount();
}
m_buf->append("{\n");
m_indent += (info.indent_delta = 2);
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
if (!m_rsrcName.empty() && m_type == Type::DebuggerSerialize) {
m_buf->append("L:");
m_buf->append(m_rsrcId);
m_buf->append(":");
m_buf->append((int)m_rsrcName.size());
m_buf->append(":\"");
m_buf->append(m_rsrcName);
m_buf->append("\"{");
} else if (!m_objClass.empty()) {
m_buf->append(m_objCode);
m_buf->append(":");
m_buf->append((int)m_objClass.size());
m_buf->append(":\"");
m_buf->append(m_objClass);
m_buf->append("\":");
m_buf->append(size);
m_buf->append(":{");
} else {
switch (kind) {
case ArrayKind::Dict:
m_buf->append("D:");
break;
case ArrayKind::Vec:
m_buf->append("v:");
break;
case ArrayKind::MarkedVArray:
m_buf->append("x:");
break;
case ArrayKind::MarkedDArray:
m_buf->append("X:");
break;
case ArrayKind::Keyset:
m_buf->append("k:");
break;
case ArrayKind::PHP:
m_buf->append("a:");
break;
case ArrayKind::VArray:
m_buf->append("y:");
break;
case ArrayKind::DArray:
m_buf->append("Y:");
break;
}
m_buf->append(size);
m_buf->append(":{");
}
break;
case Type::JSON:
info.is_vector =
(m_objClass.empty() || m_objCode == 'V' || m_objCode == 'K') &&
isVectorData &&
kind != ArrayKind::Dict;
if (info.is_vector && m_type == Type::JSON) {
info.is_vector = (m_option & k_JSON_FORCE_OBJECT)
? false : info.is_vector;
}
if (info.is_vector || kind == ArrayKind::Keyset) {
if (UNLIKELY(RuntimeOption::EvalHackArrCompatSerializeNotices) &&
kind == ArrayKind::DArray) {
if (size == 0 && m_edWarn && !m_hasEDWarned) {
raise_hackarr_compat_notice("JSON encoding empty darray");
m_hasEDWarned = true;
} else if (size != 0 && m_vdWarn && !m_hasVDWarned) {
raise_hackarr_compat_notice("JSON encoding vec-like darray");
m_hasVDWarned = true;
}
}
m_buf->append('[');
} else {
if (UNLIKELY(RuntimeOption::EvalHackArrCompatSerializeNotices) &&
kind == ArrayKind::DArray && m_ddWarn && !m_hasDDWarned) {
raise_hackarr_compat_notice("JSON encoding dict-like darray");
m_hasDDWarned = true;
}
m_buf->append('{');
}
if (m_type == Type::JSON && (m_option & k_JSON_PRETTY_PRINT) &&
info.size > 0) {
m_buf->append("\n");
m_indent += (info.indent_delta = 4);
}
break;
default:
assertx(false);
break;
}
// ...so we don't mess up next array output
if (!m_objClass.empty() || !m_rsrcName.empty()) {
m_objClass.clear();
info.is_object = true;
} else {
info.is_object = false;
}
}
void VariableSerializer::writePropertyKey(const String& prop) {
const char *key = prop.data();
int kl = prop.size();
if (!*key && kl) {
const char *cls = key + 1;
if (*cls == '*') {
assertx(key[2] == 0);
m_buf->append(key + 3, kl - 3);
const char prot[] = "\":protected";
int o = m_type == Type::PrintR ? 1 : 0;
m_buf->append(prot + o, sizeof(prot) - 1 - o);
} else {
int l = strlen(cls);
m_buf->append(cls + l + 1, kl - l - 2);
int o = m_type == Type::PrintR ? 1 : 0;
m_buf->append(&"\":\""[o], 3 - 2*o);
m_buf->append(cls, l);
const char priv[] = "\":private";
m_buf->append(priv + o, sizeof(priv) - 1 - o);
}
} else {
m_buf->append(prop);
if (m_type != Type::PrintR && m_type != Type::DebuggerDump) {
m_buf->append('"');
}
}
}
/* key MUST be a non-reference string or int */
void VariableSerializer::writeArrayKey(
const Variant& key,
VariableSerializer::ArrayKind kind
) {
using AK = VariableSerializer::ArrayKind;
auto const keyCell = tvAssertPlausible(key.asTypedValue());
bool const skey = isStringType(keyCell->m_type);
ArrayInfo &info = m_arrayInfos.back();
switch (m_type) {
case Type::DebuggerDump:
case Type::PrintR: {
indent();
if (kind == AK::Keyset) return;
m_buf->append('[');
if (info.is_object && skey) {
writePropertyKey(String{keyCell->m_data.pstr});
} else {
m_buf->append(key);
}
m_buf->append("] => ");
break;
}
case Type::VarExport:
case Type::PHPOutput:
indent();
if (kind == AK::Vec || kind == AK::Keyset) return;
if ((kind == AK::VArray || kind == AK::MarkedVArray) &&
(RO::EvalHackArrDVArrVarExport || m_type == Type::PHPOutput)) {
return;
}
write(key, true);
m_buf->append(" => ");
break;
case Type::VarDump:
case Type::DebugDump:
if (kind == AK::Vec || kind == AK::Keyset ||
kind == AK::VArray || kind == AK::MarkedVArray) {
return;
}
indent();
m_buf->append('[');
if (!skey) {
m_buf->append(keyCell->m_data.num);
} else {
m_buf->append('"');
if (info.is_object) {
writePropertyKey(String{keyCell->m_data.pstr});
} else {
m_buf->append(keyCell->m_data.pstr);
m_buf->append('"');
}
}
m_buf->append("]=>\n");
break;
case Type::APCSerialize:
if (kind == AK::Vec || kind == AK::Keyset || kind == AK::VArray) return;
if (skey) {
write(StrNR(keyCell->m_data.pstr).asString());
return;
}
case Type::Serialize:
case Type::Internal:
case Type::DebuggerSerialize:
if (kind == AK::Vec || kind == AK::MarkedVArray ||
kind == AK::Keyset || kind == AK::VArray) return;
write(key);
break;
case Type::JSON:
if (!info.is_vector && kind != ArrayKind::Keyset) {
if (!info.first_element) {
m_buf->append(',');
}
if (UNLIKELY(m_option & k_JSON_PRETTY_PRINT)) {
if (!info.first_element) {
m_buf->append("\n");
}
indent();
}
if (skey) {
auto const sdata = keyCell->m_data.pstr;
const char *k = sdata->data();
int len = sdata->size();
if (info.is_object && !*k && len) {
while (*++k) len--;
k++;
len -= 2;
}
write(k, len, true);
} else {
m_buf->append('"');
m_buf->append(keyCell->m_data.num);
m_buf->append('"');
}
m_buf->append(':');
if (UNLIKELY(m_option & k_JSON_PRETTY_PRINT)) {
m_buf->append(' ');
}
}
break;
default:
assertx(false);
break;
}
}
void VariableSerializer::writeCollectionKey(
const Variant& key,
VariableSerializer::ArrayKind kind
) {
if (m_type == Type::Serialize ||
m_type == Type::Internal ||
m_type == Type::APCSerialize ||
m_type == Type::DebuggerSerialize) {
m_valueCount++;
}
writeArrayKey(key, kind);
}
void VariableSerializer::writeArrayValue(
const Variant& value,
VariableSerializer::ArrayKind kind
) {
switch (m_type) {
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
// Do not count values in keysets because they're also keys, and it's not
// possible to have back references to keys.
if (kind != VariableSerializer::ArrayKind::Keyset) {
m_valueCount++;
}
write(value);
break;
case Type::DebuggerDump:
case Type::PrintR:
write(value);
m_buf->append('\n');
break;
case Type::VarExport:
case Type::PHPOutput: {
auto const oldKeyPrinted = m_keyPrinted;
m_keyPrinted = [&]{
if (kind == ArrayKind::Vec || kind == ArrayKind::Keyset) return false;
if ((kind == ArrayKind::VArray || kind == ArrayKind::MarkedVArray) &&
(RO::EvalHackArrDVArrVarExport || m_type == Type::PHPOutput)) {
return false;
}
return true;
}();
SCOPE_EXIT { m_keyPrinted = oldKeyPrinted; };
write(value);
m_buf->append(",\n");
break;
}
case Type::JSON: {
ArrayInfo &info = m_arrayInfos.back();
if (info.is_vector || kind == ArrayKind::Keyset) {
if (!info.first_element) {
m_buf->append(',');
}
if (UNLIKELY(m_option & k_JSON_PRETTY_PRINT)) {
if (!info.first_element) {
m_buf->append("\n");
}
indent();
}
}
write(value);
break;
}
default:
write(value);
break;
}
ArrayInfo &last_info = m_arrayInfos.back();
last_info.first_element = false;
}
void VariableSerializer::writeArrayFooter(
VariableSerializer::ArrayKind kind
) {
ArrayInfo &info = m_arrayInfos.back();
m_indent -= info.indent_delta;
switch (m_type) {
case Type::DebuggerDump:
case Type::PrintR:
if (m_rsrcName.empty()) {
indent();
m_buf->append(")\n");
if (m_indent > 0) {
m_indent -= 4;
}
}
break;
case Type::VarExport:
case Type::PHPOutput:
if (m_rsrcName.empty()) {
indent();
}
if (info.is_object && m_objCode) {
if (m_objCode == 'O') {
m_buf->append("])");
} else {
assertx(m_objCode == 'V' || m_objCode == 'K');
m_buf->append("}");
}
} else if (m_rsrcName.empty()) { // for rsrc, only write NULL in arrayHeader
switch (kind) {
case ArrayKind::Dict:
case ArrayKind::Vec:
case ArrayKind::Keyset:
m_buf->append(']');
break;
case ArrayKind::PHP:
m_buf->append(')');
break;
case ArrayKind::VArray:
case ArrayKind::DArray: {
auto const dvarrays = RO::EvalHackArrDVArrVarExport ||
m_type == Type::PHPOutput;
m_buf->append(dvarrays ? ']' : ')');
break;
}
case ArrayKind::MarkedVArray:
case ArrayKind::MarkedDArray:
always_assert(0);
}
}
break;
case Type::VarDump:
case Type::DebugDump:
if (m_rsrcName.empty()) {
indent();
m_buf->append("}\n");
}
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
m_buf->append('}');
break;
case Type::JSON:
if (m_type == Type::JSON && (m_option & k_JSON_PRETTY_PRINT) &&
info.size > 0) {
m_buf->append("\n");
indent();
}
if (info.is_vector || kind == ArrayKind::Keyset) {
m_buf->append(']');
} else {
m_buf->append('}');
}
break;
default:
assertx(false);
break;
}
m_arrayInfos.pop_back();
}
void VariableSerializer::writeSerializableObject(const String& clsname,
const String& serialized) {
m_buf->append("C:");
m_buf->append(clsname.size());
m_buf->append(":\"");
m_buf->append(clsname.data(), clsname.size());
m_buf->append("\":");
m_buf->append(serialized.size());
m_buf->append(":{");
m_buf->append(serialized.data(), serialized.size());
m_buf->append('}');
}
///////////////////////////////////////////////////////////////////////////////
void VariableSerializer::indent() {
for (int i = 0; i < m_indent; i++) {
m_buf->append(' ');
}
}
bool VariableSerializer::incNestedLevel(tv_rval tv) {
++m_currentDepth;
switch (m_type) {
case Type::VarExport:
case Type::PHPOutput:
case Type::PrintR:
case Type::VarDump:
case Type::DebugDump:
case Type::DebuggerDump:
return ++m_refs[tv].m_count >= m_maxCount;
case Type::JSON:
if (m_currentDepth > m_maxDepth) {
json_set_last_error_code(json_error_codes::JSON_ERROR_DEPTH);
}
return ++m_refs[tv].m_count >= m_maxCount;
case Type::DebuggerSerialize:
if (m_maxLevelDebugger > 0 && ++m_levelDebugger > m_maxLevelDebugger) {
return true;
}
// fall through
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
{
auto& ref = m_refs[tv];
int ct = ++ref.m_count;
bool isObject = tvIsResource(tv) || tvIsObject(tv);
if (ref.m_id != NO_ID && isObject) {
return true;
}
ref.m_id = m_valueCount;
return ct >= (m_maxCount - 1);
}
break;
default:
assertx(false);
break;
}
return false;
}
void VariableSerializer::decNestedLevel(tv_rval tv) {
--m_currentDepth;
--m_refs[tv].m_count;
if (m_type == Type::DebuggerSerialize && m_maxLevelDebugger > 0) {
--m_levelDebugger;
}
}
void VariableSerializer::serializeRFunc(const RFuncData* rfunc) {
switch (getType()) {
case Type::PrintR:
case Type::DebuggerDump:
m_buf->append("reifiedFunction{\n");
m_indent += 4;
indent();
m_buf->append("function(");
m_buf->append(rfunc->m_func->fullName()->data());
m_buf->append(")\n");
indent();
m_buf->append("[\"reified_generics\"] => ");
serializeArray(rfunc->m_arr);
m_indent -= 4;
indent();
m_buf->append("}\n");
break;
case Type::VarDump:
case Type::DebugDump:
indent();
m_buf->append("reifiedFunction{\n");
m_indent += 2;
indent();
m_buf->append("function(");
m_buf->append(rfunc->m_func->fullName()->data());
m_buf->append(")\n");
indent();
m_buf->append("[\"reified_generics\"]=>\n");
serializeArray(rfunc->m_arr);
m_indent -= 2;
indent();
m_buf->append("}\n");
break;
case Type::VarExport:
case Type::Serialize:
case Type::Internal:
case Type::JSON:
case Type::APCSerialize:
case Type::DebuggerSerialize:
case Type::PHPOutput:
SystemLib::throwInvalidOperationExceptionObject(
"Unable to serialize reified function pointer"
);
break;
}
}
void VariableSerializer::serializeFunc(const Func* func) {
auto const name = func->fullName();
switch (getType()) {
case Type::VarExport:
case Type::PHPOutput:
m_buf->append("fun(");
write(name->data(), name->size());
m_buf->append(')');
break;
case Type::VarDump:
case Type::DebugDump:
// TODO (T29639296)
// For now we use function(foo) to dump function pointers in most cases,
// and this can be changed in the future.
indent();
m_buf->append("function(");
m_buf->append(name->data());
m_buf->append(")\n");
break;
case Type::PrintR:
case Type::DebuggerDump:
m_buf->append("function(");
m_buf->append(name->data());
m_buf->append(')');
break;
case Type::JSON:
if (func->isMethCaller()) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerSerde.get()}
);
}
write(func->nameStr());
break;
case Type::APCSerialize:
if (func->isMethCaller()) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerAPC.get()}
);
}
case Type::Serialize:
case Type::Internal:
case Type::DebuggerSerialize:
if (func->isMethCaller()) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerSerde.get()}
);
}
invalidFuncConversion("string");
break;
}
}
void VariableSerializer::serializeClass(const Class* cls) {
switch (getType()) {
case Type::VarExport:
case Type::PHPOutput:
if (RuntimeOption::EvalClassAsStringVarExport) {
write(StrNR(cls->name()));
} else {
m_buf->append(cls->name());
m_buf->append("::class");
}
break;
case Type::VarDump:
if (RuntimeOption::EvalClassAsStringVarDump) {
write(StrNR(cls->name()));
break;
}
// fall-through
case Type::DebugDump:
indent();
m_buf->append("class(");
m_buf->append(cls->name());
m_buf->append(")\n");
break;
case Type::PrintR:
case Type::DebuggerDump:
m_buf->append("class(");
m_buf->append(cls->name());
m_buf->append(')');
break;
case Type::JSON:
write(StrNR(classToStringHelper(cls)));
break;
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize:
write(StrNR(classToStringHelper(cls)));
break;
}
}
void VariableSerializer::serializeLazyClass(LazyClassData lcls) {
switch (getType()) {
case Type::VarExport:
case Type::PHPOutput:
if (RuntimeOption::EvalClassAsStringVarExport) {
write(StrNR(lcls.name()));
} else {
m_buf->append(lcls.name());
m_buf->append("::class");
}
break;
case Type::VarDump:
if (RuntimeOption::EvalClassAsStringVarDump) {
write(StrNR(lcls.name()));
break;
}
// fall-through
case Type::DebugDump:
indent();
m_buf->append("class(");
m_buf->append(lcls.name());
m_buf->append(")\n");
break;
case Type::PrintR:
case Type::DebuggerDump:
m_buf->append("class(");
m_buf->append(lcls.name());
m_buf->append(')');
break;
case Type::JSON:
case Type::Serialize:
case Type::DebuggerSerialize:
write(StrNR(lazyClassToStringHelper(lcls)));
break;
case Type::Internal:
case Type::APCSerialize: {
auto cname = lcls.name();
m_buf->append("l:");
m_buf->append(cname->size());
m_buf->append(":\"");
m_buf->append(cname->data(), cname->size());
m_buf->append("\";");
break;
}
}
}
void VariableSerializer::serializeClsMeth(
ClsMethDataRef clsMeth, bool skipNestCheck /* = false */) {
auto const clsName = clsMeth->getCls()->name();
auto const funcName = clsMeth->getFunc()->name();
switch (getType()) {
case Type::DebuggerDump:
case Type::PrintR:
m_buf->append("classMeth{\n");
m_indent += 4;
indent();
m_buf->append("class(");
m_buf->append(clsName->data());
m_buf->append(")\n");
indent();
m_buf->append("function(");
m_buf->append(funcName->data());
m_buf->append(")\n");
m_indent -= 4;
indent();
m_buf->append("}");
break;
case Type::VarExport:
case Type::PHPOutput:
m_buf->append("class_meth(");
write(clsName->data(), clsName->size());
m_buf->append(", ");
write(funcName->data(), funcName->size());
m_buf->append(')');
break;
case Type::VarDump:
case Type::DebugDump:
indent();
m_buf->append("classMeth{\n");
m_indent += 2;
indent();
m_buf->append("class(");
m_buf->append(clsName->data());
m_buf->append(")\n");
indent();
m_buf->append("function(");
m_buf->append(funcName->data());
m_buf->append(")\n");
m_indent -= 2;
indent();
m_buf->append("}\n");
break;
case Type::JSON: {
auto const kind = getKind(empty_vec_array().get());
writeArrayHeader(2 /* size */, true /* isVectorData */, kind);
writeArrayKey(VarNR(0), kind);
writeArrayValue(VarNR(clsName), kind);
writeArrayKey(VarNR(1), kind);
writeArrayValue(VarNR(funcName), kind);
writeArrayFooter(kind);
break;
}
case Type::Serialize:
case Type::Internal:
case Type::APCSerialize:
case Type::DebuggerSerialize: {
SystemLib::throwInvalidOperationExceptionObject(
"Unable to serialize class meth pointer"
);
}
}
}
void VariableSerializer::serializeRClsMeth(RClsMethData* rclsMeth) {
switch (getType()) {
case Type::PrintR:
case Type::DebuggerDump:
m_buf->append("reifiedClassMeth{\n");
m_indent += 4;
indent();
m_buf->append("class(");
m_buf->append(rclsMeth->m_cls->name()->data(), rclsMeth->m_cls->name()->size());
m_buf->append(")\n");
indent();
m_buf->append("function(");
m_buf->append(rclsMeth->m_func->name()->data());
m_buf->append(")\n");
indent();
m_buf->append("[\"reified_generics\"] => ");
serializeArray(rclsMeth->m_arr);
m_indent -= 4;
indent();
m_buf->append("}\n");
break;
case Type::VarDump:
case Type::DebugDump:
indent();
m_buf->append("reifiedClassMeth{\n");
m_indent += 2;
indent();
m_buf->append("class(");
m_buf->append(rclsMeth->m_cls->name()->data(), rclsMeth->m_cls->name()->size());
m_buf->append(")\n");
indent();
m_buf->append("function(");
m_buf->append(rclsMeth->m_func->name()->data());
m_buf->append(")\n");
indent();
m_buf->append("[\"reified_generics\"]=>\n");
serializeArray(rclsMeth->m_arr);
m_indent -= 2;
indent();
m_buf->append("}\n");
break;
case Type::VarExport:
case Type::Serialize:
case Type::Internal:
case Type::JSON:
case Type::APCSerialize:
case Type::DebuggerSerialize:
case Type::PHPOutput:
SystemLib::throwInvalidOperationExceptionObject(
"Unable to serialize reified class meth pointer"
);
break;
}
}
NEVER_INLINE
void VariableSerializer::serializeVariant(tv_rval tv,
bool isArrayKey /* = false */,
bool skipNestCheck /* = false */,
bool noQuotes /* = false */) {
switch (type(tv)) {
case KindOfUninit:
case KindOfNull:
assertx(!isArrayKey);
writeNull();
return;
case KindOfBoolean:
assertx(!isArrayKey);
write(val(tv).num != 0);
return;
case KindOfInt64:
write(val(tv).num);
return;
case KindOfDouble:
write(val(tv).dbl);
return;
case KindOfPersistentString:
case KindOfString:
write(val(tv).pstr->data(),
val(tv).pstr->size(), isArrayKey, noQuotes);
return;
case KindOfPersistentVec:
case KindOfVec:
case KindOfPersistentDict:
case KindOfDict:
case KindOfPersistentKeyset:
case KindOfKeyset:
assertx(!isArrayKey);
serializeArray(val(tv).parr, skipNestCheck);
return;
case KindOfObject:
assertx(!isArrayKey);
serializeObject(val(tv).pobj);
return;
case KindOfResource:
assertx(!isArrayKey);
serializeResource(val(tv).pres->data());
return;
case KindOfRFunc:
assertx(!isArrayKey);
serializeRFunc(val(tv).prfunc);
return;
case KindOfFunc:
assertx(!isArrayKey);
serializeFunc(val(tv).pfunc);
return;
case KindOfClass:
assertx(!isArrayKey);
serializeClass(val(tv).pclass);
return;
case KindOfClsMeth:
assertx(!isArrayKey);
serializeClsMeth(val(tv).pclsmeth, skipNestCheck);
return;
case KindOfRClsMeth:
assertx(!isArrayKey);
serializeRClsMeth(val(tv).prclsmeth);
return;
case KindOfLazyClass:
assertx(!isArrayKey);
serializeLazyClass(val(tv).plazyclass);
return;
}
not_reached();
}
void VariableSerializer::serializeResourceImpl(const ResourceData* res) {
pushResourceInfo(res->o_getResourceName(), res->getId());
serializeArray(ArrayData::CreateDict());
popResourceInfo();
}
void VariableSerializer::serializeResource(const ResourceData* res) {
TypedValue tv = make_tv<KindOfResource>(const_cast<ResourceHdr*>(res->hdr()));
if (UNLIKELY(incNestedLevel(&tv))) {
writeOverflow(&tv);
} else if (auto trace = dynamic_cast<const CompactTrace*>(res)) {
auto const trace_array = Variant(trace->extract());
auto const raw = *trace_array.asTypedValue();
// We use a depth of 2 because backtrace arrays are varrays-of-darrays.
auto const marked = Variant::attach(arrprov::markTvToDepth(raw, true, 2));
serializeArray(marked.toArray().get());
} else {
serializeResourceImpl(res);
}
decNestedLevel(&tv);
}
void VariableSerializer::serializeString(const String& str) {
if (str) {
write(str.data(), str.size());
} else {
writeNull();
}
}
void VariableSerializer::serializeArrayImpl(const ArrayData* arr,
bool isVectorData) {
using AK = VariableSerializer::ArrayKind;
AK kind = getKind(arr);
writeArrayHeader(arr->size(), isVectorData, kind);
IterateKV(
arr,
[&](TypedValue k, TypedValue v) {
writeArrayKey(VarNR(k), kind);
writeArrayValue(VarNR(v), kind);
}
);
writeArrayFooter(kind);
}
void VariableSerializer::serializeArray(const ArrayData* arr,
bool skipNestCheck /* = false */) {
if (UNLIKELY(RuntimeOption::EvalHackArrCompatSerializeNotices)) {
if (UNLIKELY(m_hackWarn && !m_hasHackWarned)) {
raise_hack_arr_compat_serialize_notice(arr);
m_hasHackWarned = true;
}
if (UNLIKELY(m_dictWarn && !m_hasDictWarned && arr->isDictType())) {
raise_hack_arr_compat_serialize_notice(arr);
m_hasDictWarned = true;
}
if (UNLIKELY(m_keysetWarn && !m_hasKeysetWarned && arr->isKeysetType())) {
raise_hack_arr_compat_serialize_notice(arr);
m_hasKeysetWarned = true;
}
}
const bool isVectorData = arr->isVectorData();
if (arr->empty()) {
auto const kind = getKind(arr);
writeArrayHeader(0, isVectorData, kind);
writeArrayFooter(kind);
return;
}
if (!skipNestCheck) {
TypedValue tv = make_array_like_tv(const_cast<ArrayData*>(arr));
if (incNestedLevel(&tv)) {
writeOverflow(&tv);
} else {
serializeArrayImpl(arr, isVectorData);
}
decNestedLevel(&tv);
} else {
// If skipNestCheck, the array is temporary and we should not check or
// save its pointer. We'll serialize it without its header.
serializeArrayImpl(arr, isVectorData);
}
}
void VariableSerializer::serializeObjProps(Array& arr) {
if (arr.isNull()) {
writeNull();
return;
}
auto const ad = arr.detach();
auto const dict = ad->toDict(ad->cowCheck());
if (dict != ad) decRefArr(ad);
serializeArray(dict, /*skipNestCheck=*/true);
decRefArr(dict);
}
void VariableSerializer::serializeCollection(ObjectData* obj) {
using AK = VariableSerializer::ArrayKind;
int64_t sz = collections::getSize(obj);
auto type = obj->collectionType();
if (isMapCollection(type)) {
pushObjectInfo(obj->getClassName(),'K');
writeArrayHeader(sz, false, AK::PHP);
for (ArrayIter iter(obj); iter; ++iter) {
writeCollectionKey(iter.first(), AK::PHP);
writeArrayValue(iter.second(), AK::PHP);
}
writeArrayFooter(AK::PHP);
} else {
assertx(isVectorCollection(type) ||
isSetCollection(type) ||
(type == CollectionType::Pair));
pushObjectInfo(obj->getClassName(), 'V');
writeArrayHeader(sz, true, AK::PHP);
auto ser_type = getType();
if (ser_type == VariableSerializer::Type::Serialize ||
ser_type == VariableSerializer::Type::Internal ||
ser_type == VariableSerializer::Type::APCSerialize ||
ser_type == VariableSerializer::Type::DebuggerSerialize ||
ser_type == VariableSerializer::Type::VarExport ||
ser_type == VariableSerializer::Type::PHPOutput) {
// For the 'V' serialization format, we don't print out keys
// for Serialize, APCSerialize, DebuggerSerialize
bool const should_indent =
ser_type == VariableSerializer::Type::VarExport ||
ser_type == VariableSerializer::Type::PHPOutput;
for (ArrayIter iter(obj); iter; ++iter) {
if (should_indent) {
indent();
}
writeArrayValue(iter.second(), AK::PHP);
}
} else {
if (isSetCollection(type)) {
bool const should_indent =
ser_type == VariableSerializer::Type::PrintR ||
ser_type == VariableSerializer::Type::DebuggerDump;
for (ArrayIter iter(obj); iter; ++iter) {
if (should_indent) {
indent();
}
writeArrayValue(iter.second(), AK::PHP);
}
} else {
for (ArrayIter iter(obj); iter; ++iter) {
writeCollectionKey(iter.first(), AK::PHP);
writeArrayValue(iter.second(), AK::PHP);
}
}
}
writeArrayFooter(AK::PHP);
}
popObjectInfo();
}
/* Get properties from the actual object unless we're
* serializing for var_dump()/print_r() and the object
* exports a __debugInfo() magic method.
* In which case, call that and use the array it returns.
*/
Array VariableSerializer::getSerializeProps(const ObjectData* obj) const {
if (getType() == VariableSerializer::Type::VarExport) {
Array props = Array::CreateDict();
for (ArrayIter iter(obj->toArray(false, true)); iter; ++iter) {
auto key = iter.first().toString();
// Jump over any class attribute mangling
if (key[0] == '\0' && key.size() > 0) {
int sizeToCut = 0;
do {
sizeToCut++;
} while (key[sizeToCut] != '\0');
key = key.substr(sizeToCut+1);
}
props.set(key, iter.secondVal());
}
return props;
}
if ((getType() != VariableSerializer::Type::PrintR) &&
(getType() != VariableSerializer::Type::VarDump)) {
auto const ignoreLateInit =
(m_ignoreLateInit ||
getType() == VariableSerializer::Type::DebugDump ||
getType() == VariableSerializer::Type::DebuggerDump ||
getType() == VariableSerializer::Type::DebuggerSerialize);
return obj->toArray(false, ignoreLateInit);
}
auto cls = obj->getVMClass();
auto debuginfo = cls->lookupMethod(s_debugInfo.get());
if (!debuginfo) {
// When ArrayIterator is cast to an array, it returns its array object,
// however when it's being var_dump'd or print_r'd, it shows its properties
if (UNLIKELY(obj->instanceof(SystemLib::s_ArrayIteratorClass))) {
auto ret = Array::CreateDict();
obj->o_getArray(ret, false, true);
return ret;
}
// Same with Closure, since it's a dynamic object but still has its own
// different behavior for var_dump and cast to array
if (UNLIKELY(obj->instanceof(c_Closure::classof()))) {
auto ret = Array::CreateDict();
obj->o_getArray(ret, false, true);
return ret;
}
return obj->toArray(false, true);
}
if (debuginfo->attrs() & (AttrPrivate|AttrProtected|
AttrAbstract|AttrStatic)) {
raise_warning("%s::__debugInfo() must be public and non-static",
cls->name()->data());
return obj->toArray(false, true);
}
auto const providedCoeffects =
m_pure ? RuntimeCoeffects::pure() : RuntimeCoeffects::defaults();
auto ret = const_cast<ObjectData*>(obj)->invokeDebugInfo(providedCoeffects);
if (ret.isArray()) {
return ret.toArray();
}
if (ret.isNull()) {
return empty_dict_array();
}
raise_error("__debugInfo() must return an array");
not_reached();
}
void VariableSerializer::serializeObjectImpl(const ObjectData* obj) {
bool handleSleep = false;
Variant serializableNativeData = init_null();
Variant ret;
auto const type = getType();
if (obj->isCollection()) {
serializeCollection(const_cast<ObjectData*>(obj));
return;
}
if (RO::EvalForbidMethCallerHelperSerialize &&
(type == Type::Serialize || type == Type::Internal ||
type == Type::DebuggerSerialize || type == Type::JSON) &&
obj->getVMClass() == SystemLib::s_MethCallerHelperClass) {
if (RO::EvalForbidMethCallerHelperSerialize == 1) {
raise_warning("Serializing MethCallerHelper");
} else {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerSerde.get()}
);
}
}
if (m_disallowObjects) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_disallowedObjectSerde.get()}
);
}
if (LIKELY(type == VariableSerializer::Type::Serialize ||
type == VariableSerializer::Type::Internal ||
type == VariableSerializer::Type::APCSerialize)) {
if (obj->instanceof(SystemLib::s_SerializableClass)) {
assertx(!obj->isCollection());
ret =
const_cast<ObjectData*>(obj)->o_invoke_few_args(s_serialize, RuntimeCoeffects::fixme(), 0);
if (ret.isString()) {
writeSerializableObject(obj->getClassName(), ret.toString());
} else if (ret.isNull()) {
writeNull();
} else {
raise_error("%s::serialize() must return a string or NULL",
obj->getClassName().data());
}
return;
}
// Only serialize CPP extension type instances which can actually
// be deserialized. Otherwise, raise a warning and serialize
// null.
// Similarly, do not try to serialize WaitHandles
// as they contain internal state via non-NativeData means.
auto cls = obj->getVMClass();
if ((cls->instanceCtor() && !cls->isCppSerializable()) ||
obj->isWaitHandle()) {
raise_warning("Attempted to serialize unserializable builtin class %s",
obj->getVMClass()->preClass()->name()->data());
serializeVariant(init_null().asTypedValue());
return;
}
if (type == VariableSerializer::Type::APCSerialize) {
if (cls == SystemLib::s_MethCallerHelperClass) {
if (RO::EvalForbidMethCallerAPCSerialize == 1) {
raise_warning("Storing meth_caller in APC");
} else if (RO::EvalForbidMethCallerAPCSerialize > 1) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerAPC.get()}
);
}
} else if (cls == SystemLib::s_DynMethCallerHelperClass) {
SystemLib::throwInvalidOperationExceptionObject(
VarNR{s_invalidMethCallerAPC.get()}
);
}
}
if (obj->getVMClass()->rtAttribute(Class::HasSleep)) {
handleSleep = true;
auto const providedCoeffects =
m_pure ? RuntimeCoeffects::pure() : RuntimeCoeffects::defaults();
ret = const_cast<ObjectData*>(obj)->invokeSleep(providedCoeffects);
}
if (obj->hasNativeData()) {
auto* ndi = cls->getNativeDataInfo();
if (ndi->isSerializable()) {
serializableNativeData = Native::nativeDataSleep(obj);
}
}
} else if (UNLIKELY(type == VariableSerializer::Type::DebuggerSerialize)) {
// Don't try to serialize a CPP extension class which doesn't
// support serialization. Just send the class name instead.
if (obj->isCppBuiltin() && !obj->getVMClass()->isCppSerializable()) {
write(obj->getClassName());
return;
}
if (obj->hasNativeData() &&
obj->getVMClass()->getNativeDataInfo()->isSerializable()) {
serializableNativeData = Native::nativeDataSleep(obj);
}
}
if (UNLIKELY(handleSleep)) {
assertx(!obj->isCollection());
if (ret.isArray()) {
Array wanted = Array::CreateDict();
assertx(isArrayLikeType(ret.getType()));
const Array &props = ret.asCArrRef();
for (ArrayIter iter(props); iter; ++iter) {
String memberName = iter.second().toString();
String propName = memberName;
auto obj_cls = obj->getVMClass();
Class* ctx = obj_cls;
auto attrMask = AttrNone;
if (memberName.data()[0] == 0) {
int subLen = memberName.find('\0', 1) + 1;
if (subLen > 2) {
if (subLen == 3 && memberName.data()[1] == '*') {
attrMask = AttrProtected;
memberName = memberName.substr(subLen);
} else {
attrMask = AttrPrivate;
String cls = memberName.substr(1, subLen - 2);
ctx = Class::lookup(cls.get());
if (ctx) {
memberName = memberName.substr(subLen);
} else {
ctx = obj_cls;
}
}
}
}
auto const lookup = obj_cls->getDeclPropSlot(ctx, memberName.get());
auto const slot = lookup.slot;
if (slot != kInvalidSlot && lookup.accessible) {
auto propVal = const_cast<ObjectData*>(obj)->propLvalAtOffset(slot);
auto const& prop = obj_cls->declProperties()[slot];
if (propVal.type() != KindOfUninit) {
if (prop.attrs & AttrPrivate) {
memberName = concat4(s_zero, ctx->nameStr(),
s_zero, memberName);
} else if (prop.attrs & AttrProtected) {
memberName = concat(s_protected_prefix, memberName);
}
if (!attrMask || (attrMask & prop.attrs) == attrMask) {
wanted.set(memberName, propVal.tv());
continue;
}
} else if (prop.attrs & AttrLateInit) {
if (m_ignoreLateInit) {
continue;
} else {
throw_late_init_prop(prop.cls, memberName.get(), false);
}
}
}
if (!attrMask &&
UNLIKELY(obj->getAttribute(ObjectData::HasDynPropArr))) {
auto const prop = obj->dynPropArray()->get(propName.get());
if (prop.is_init()) {
wanted.set(propName, prop);
continue;
}
}
raise_notice("serialize(): \"%s\" returned as member variable from "
"__sleep() but does not exist", propName.data());
wanted.set(propName, init_null());
}
pushObjectInfo(obj->getClassName(), 'O');
if (!serializableNativeData.isNull()) {
wanted.set(s_serializedNativeDataKey, serializableNativeData);
}
serializeObjProps(wanted);
popObjectInfo();
} else {
raise_notice("serialize(): __sleep should return an array only "
"containing the names of instance-variables to "
"serialize");
serializeVariant(uninit_null().asTypedValue());
}
} else {
if (type == VariableSerializer::Type::VarExport &&
obj->instanceof(c_Closure::classof())) {
write(obj->getClassName());
} else {
auto className = obj->getClassName();
Array properties = getSerializeProps(obj);
if (type == VariableSerializer::Type::DebuggerSerialize) {
try {
CoeffectsAutoGuard _;
auto val = const_cast<ObjectData*>(obj)->invokeToDebugDisplay(
RuntimeCoeffects::automatic());
if (val.isInitialized()) {
properties.set(s_PHP_DebugDisplay, *val.asTypedValue());
}
} catch (const Object &e) {
assertx(e->instanceof(SystemLib::s_ErrorClass) ||
e->instanceof(SystemLib::s_ExceptionClass));
assertx(
SystemLib::s_ErrorClass->lookupDeclProp(s_message.get()) == 0 &&
SystemLib::s_ExceptionClass->lookupDeclProp(s_message.get()) == 0
);
auto const message_rval = e->propRvalAtOffset(Slot{0});
if (isStringType(message_rval.type())) {
raise_warning("%s::__toDebugDisplay() threw PHP exception "
"of class %s with message '%s'",
obj->getClassName().data(), e->getClassName().data(),
message_rval.val().pstr->data());
} else {
raise_warning("%s::__toDebugDisplay() threw PHP exception "
"of class %s with non-string message",
obj->getClassName().data(), e->getClassName().data());
}
} catch (const std::exception &e) {
raise_warning("%s::__toDebugDisplay() threw C++ exception: %s",
obj->getClassName().data(), e.what());
} catch (...) {
raise_warning("%s::__toDebugDisplay() threw unknown exception",
obj->getClassName().data());
}
}
if (type == VariableSerializer::Type::DebuggerDump) {
// Expect to display as their stringified classname.
if (obj->instanceof(c_Closure::classof())) {
write(obj->getVMClass()->nameStr());
return;
}
// If we have a DebugDisplay prop saved, use it.
auto const debugDisp = obj->getProp(nullptr, s_PHP_DebugDisplay.get());
if (debugDisp) {
serializeVariant(debugDisp, false, false, true);
return;
}
// Otherwise compute it if we have a __toDebugDisplay method.
CoeffectsAutoGuard _;
auto val = const_cast<ObjectData*>(obj)->invokeToDebugDisplay(
RuntimeCoeffects::automatic());
if (val.isInitialized()) {
serializeVariant(val.asTypedValue(), false, false, true);
return;
}
}
if (className.get() == s_PHP_Incomplete_Class.get() &&
(type == VariableSerializer::Type::Serialize ||
type == VariableSerializer::Type::Internal ||
type == VariableSerializer::Type::APCSerialize ||
type == VariableSerializer::Type::DebuggerSerialize ||
type == VariableSerializer::Type::DebuggerDump)) {
auto const cname = obj->getProp(
nullptr,
s_PHP_Incomplete_Class_Name.get()
);
if (cname && isStringType(cname.type())) {
pushObjectInfo(StrNR(cname.val().pstr), 'O');
properties.remove(s_PHP_Incomplete_Class_Name, true);
serializeObjProps(properties);
popObjectInfo();
return;
}
}
pushObjectInfo(className, 'O');
if (!serializableNativeData.isNull()) {
properties.set(s_serializedNativeDataKey, serializableNativeData);
}
serializeObjProps(properties);
popObjectInfo();
}
}
}
void VariableSerializer::serializeObject(const ObjectData* obj) {
TypedValue tv = make_tv<KindOfObject>(const_cast<ObjectData*>(obj));
if (UNLIKELY(incNestedLevel(&tv))) {
writeOverflow(&tv);
} else {
serializeObjectImpl(obj);
}
decNestedLevel(&tv);
}
void VariableSerializer::serializeObject(const Object& obj) {
if (obj) {
serializeObject(obj.get());
} else {
writeNull();
}
}
}