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(); } } }