Program: CWE-416
Project: CVE-2020-1900
Buggy source:
/*
   +----------------------------------------------------------------------+
   | 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-unserializer.h"

#include <algorithm>
#include <utility>

#include <folly/Conv.h>
#include <folly/Range.h>
#include <folly/lang/Launder.h>

#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/autoload-handler.h"
#include "hphp/runtime/base/set-array.h"
#include "hphp/runtime/base/collections.h"
#include "hphp/runtime/base/comparisons.h"
#include "hphp/runtime/base/dummy-resource.h"
#include "hphp/runtime/base/execution-context.h"
#include "hphp/runtime/base/runtime-error.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/struct-log-util.h"
#include "hphp/runtime/base/request-info.h"
#include "hphp/runtime/base/variable-serializer.h"

#include "hphp/runtime/ext/collections/ext_collections-map.h"
#include "hphp/runtime/ext/collections/ext_collections-pair.h"
#include "hphp/runtime/ext/collections/ext_collections-set.h"
#include "hphp/runtime/ext/collections/ext_collections-vector.h"
#include "hphp/runtime/ext/std/ext_std_classobj.h"

#include "hphp/runtime/vm/native-data.h"
#include "hphp/runtime/vm/repo-global-data.h"
#include "hphp/runtime/vm/repo.h"

#include "hphp/runtime/vm/jit/perf-counters.h"

#include "hphp/zend/zend-strtod.h"

namespace HPHP {

namespace {

enum class ArrayKind { PHP, Dict, Vec, Keyset };

[[noreturn]] NEVER_INLINE
void throwUnexpectedSep(char expect, char actual) {
  throw Exception("Expected '%c' but got '%c'", expect, actual);
}

[[noreturn]] NEVER_INLINE
void throwOutOfRange(int64_t id) {
  throw Exception("Id %" PRId64 " out of range", id);
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedStr(const char* expect, folly::StringPiece& actual) {
  throw Exception("Expected '%s' but got '%.*s'", expect,
                  (int)actual.size(), actual.data());
}

[[noreturn]] NEVER_INLINE
void throwUnknownType(char type) {
  throw Exception("Unknown type '%c'", type);
}

[[noreturn]] NEVER_INLINE
void throwInvalidPair() {
  throw Exception("Pair objects must have exactly 2 elements");
}

[[noreturn]] NEVER_INLINE
void throwInvalidOFormat(const String& clsName) {
  throw Exception("%s does not support the 'O' serialization format",
                  clsName.data());
}

[[noreturn]] NEVER_INLINE
void throwMangledPrivateProperty() {
  throw Exception("Mangled private object property");
}

[[noreturn]] NEVER_INLINE
void throwUnterminatedProperty() {
  throw Exception("Object property not terminated properly");
}

[[noreturn]] NEVER_INLINE
void throwNotCollection(const String& clsName) {
  throw Exception("%s is not a collection class", clsName.data());
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedType(const String& key, const ObjectData* obj,
                         TypedValue type) {
  auto msg = folly::format(
    "Property {} for class {} was deserialized with type ({}) that "
    "didn't match what we inferred in static analysis",
    key,
    obj->getVMClass()->name(),
    tname(type.m_type)
  ).str();
  throw Exception(msg);
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedType(const StringData* key, const ObjectData* obj,
                         TypedValue type) {
  String str(key->data(), key->size(), CopyString);
  throwUnexpectedType(str, obj, type);
}

[[noreturn]] NEVER_INLINE
void throwArraySizeOutOfBounds() {
  throw Exception("Array size out of bounds");
}

[[noreturn]] NEVER_INLINE
void throwInvalidKey() {
  throw Exception("Invalid key");
}

[[noreturn]] NEVER_INLINE
void throwUnterminatedElement() {
  throw Exception("Array element not terminated properly");
}

[[noreturn]] NEVER_INLINE
void throwLargeStringSize(int64_t size) {
  throw Exception("Size of serialized string (%" PRId64 ") exceeds max", size);
}

[[noreturn]] NEVER_INLINE
void throwNegativeStringSize(int64_t size) {
  throw Exception("Size of serialized string (%" PRId64 ") "
                  "must not be negative", size);
}

[[noreturn]] NEVER_INLINE
void throwBadFormat(const ObjectData* obj, char type) {
  throw Exception("%s does not support the '%c' serialization format",
                  header_names[(int)obj->headerKind()], type);
}

[[noreturn]] NEVER_INLINE
void throwInvalidHashKey(const ObjectData* obj) {
  throw Exception("%s values must be integers or strings",
                  header_names[(int)obj->headerKind()]);
}

[[noreturn]] NEVER_INLINE
void throwColRKey() {
  throw Exception("Referring to collection keys using the 'r' encoding "
                    "is not supported");
}

[[noreturn]] NEVER_INLINE
void throwColRefValue() {
  throw Exception("Collection values cannot be taken by reference");
}

[[noreturn]] NEVER_INLINE
void throwColRefKey() {
  throw Exception("Collection keys cannot be taken by reference");
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedEOB() {
  throw Exception("Unexpected end of buffer during unserialization");
}

[[noreturn]] NEVER_INLINE
void throwVecRefValue() {
  throw Exception("Vecs cannot contain references");
}

[[noreturn]] NEVER_INLINE
void throwDictRefValue() {
  throw Exception("Dicts cannot contain references");
}

[[noreturn]] NEVER_INLINE
void throwKeysetValue() {
  throw Exception("Keysets can only contain integers and strings");
}

[[noreturn]] NEVER_INLINE
void throwInvalidClassName() {
  throw Exception("Provided class name is invalid");
}

void warnOrThrowUnknownClass(const String& clsName) {
  if (RuntimeOption::EvalForbidUnserializeIncompleteClass) {
    auto const msg = folly::sformat(
      "Attempted to unserialize class named '{}' but it doesn't exist",
      clsName.toCppString()
    );
    if (RuntimeOption::EvalForbidUnserializeIncompleteClass > 1) {
      throw_object("Exception", make_vec_array(msg));
    } else {
      raise_warning(msg);
    }
  }
}
}

const StaticString
  s_serialized("serialized"),
  s_unserialize("unserialize"),
  s_PHP_Incomplete_Class("__PHP_Incomplete_Class"),
  s_PHP_Incomplete_Class_Name("__PHP_Incomplete_Class_Name"),
  s___wakeup("__wakeup");

///////////////////////////////////////////////////////////////////////////////

const StaticString s_force_darrays{"force_darrays"};
const StaticString s_mark_legacy_arrays{"mark_legacy_arrays"};

VariableUnserializer::VariableUnserializer(
  const char* str,
  size_t len,
  Type type,
  bool allowUnknownSerializableClass,
  const Array& options)
    : m_type(type)
    , m_readOnly(false)
    , m_buf(str)
    , m_end(str + len)
    , m_unknownSerializable(allowUnknownSerializableClass)
    , m_options(options)
    , m_begin(str)
    , m_forceDArrays{m_options[s_force_darrays].toBoolean()}
    , m_markLegacyArrays{m_options[s_mark_legacy_arrays].toBoolean()}
{}

VariableUnserializer::Type VariableUnserializer::type() const {
  return m_type;
}

bool VariableUnserializer::allowUnknownSerializableClass() const {
  return m_unknownSerializable;
}

const char* VariableUnserializer::head() const {
  return m_buf;
}

const char* VariableUnserializer::begin() const {
  return m_begin;
}

const char* VariableUnserializer::end() const {
  return m_end;
}

char VariableUnserializer::peek() const {
  check();
  return *m_buf;
}

char VariableUnserializer::peekBack() const {
  return m_buf[-1];
}

bool VariableUnserializer::endOfBuffer() const {
  return m_buf >= m_end;
}

char VariableUnserializer::readChar() {
  check();
  return *(m_buf++);
}

void VariableUnserializer::add(tv_lval v, UnserializeMode mode) {
  switch (mode) {
    case UnserializeMode::Value:  m_refs.emplace_back(v); break;
    // We don't support refs to collection keys; use nullptr as a sentinel.
    case UnserializeMode::ColKey: m_refs.emplace_back(nullptr); break;
    case UnserializeMode::Key:    break;
  }
}

void VariableUnserializer::reserveForAdd(size_t count) {
  // If the array is large, the space for the backrefs could be
  // significant, so we need to check for OOM beforehand. To do this,
  // we need to do some guess work to estimate what memory the vector
  // will consume once we've done the reserve (we assume the vector
  // doubles in capacity as necessary).
  auto const newSize = m_refs.size() + count;
  auto const capacity = m_refs.capacity();
  if (newSize <= capacity) return;
  auto const total =
    (folly::nextPowTwo(newSize) - capacity) *
    sizeof(decltype(m_refs)::value_type);
  if (UNLIKELY(total > kMaxSmallSize && tl_heap->preAllocOOM(total))) {
    check_non_safepoint_surprise();
  }
  m_refs.reserve(newSize);
}

TypedValue VariableUnserializer::getByVal(int id) {
  if (id <= 0 || id > m_refs.size()) throwOutOfRange(id);
  auto const result = m_refs[id - 1];
  if (!result) throwColRKey();
  return result.tv();
}

void VariableUnserializer::check() const {
  if (m_buf >= m_end) throwUnexpectedEOB();
}

void VariableUnserializer::checkElemTermination() const {
  auto const ch = peekBack();
  if (ch != ';' && ch != '}') throwUnterminatedElement();
}

void VariableUnserializer::set(const char* buf, const char* end) {
  m_buf = buf;
  m_end = end;
}

Variant VariableUnserializer::unserialize() {
  Variant v;
  unserializeVariant(v.asTypedValue());
  if (UNLIKELY(StructuredLog::coinflip(RuntimeOption::EvalSerDesSampleRate))) {
    String ser(m_begin, m_end - m_begin, CopyString);
    auto const fmt = folly::sformat("VU{}", (int)m_type);
    StructuredLog::logSerDes(fmt.c_str(), "des", ser, v);
  }

  for (auto& obj : m_sleepingObjects) {
    obj->invokeWakeup();
  }

  return v;
}

namespace {
std::pair<int64_t,const char*> hh_strtoll_base10(const char* p) {
  int64_t x = 0;
  bool neg = false;
  if (*p == '-') {
    neg = true;
    ++p;
  }
  while (*p >= '0' && *p <= '9') {
    x = (x * 10) + ('0' - *p);
    ++p;
  }
  if (!neg) {
    x = -x;
  }
  return std::pair<int64_t,const char*>(x, p);
}
}

int64_t VariableUnserializer::readInt() {
  check();
  auto r = hh_strtoll_base10(m_buf);
  m_buf = r.second;
  return r.first;
}

double VariableUnserializer::readDouble() {
  check();
  const char* newBuf;
  double r = zend_strtod(m_buf, &newBuf);
  m_buf = newBuf;
  return r;
}

folly::StringPiece VariableUnserializer::readStr(unsigned n) {
  check();
  auto const bufferLimit = std::min(size_t(m_end - m_buf), size_t(n));
  auto str = folly::StringPiece(m_buf, bufferLimit);
  m_buf += bufferLimit;
  return str;
}

void VariableUnserializer::expectChar(char expected) {
  char ch = readChar();
  if (UNLIKELY(ch != expected)) {
    throwUnexpectedSep(expected, ch);
  }
}

namespace {
bool isWhitelistClass(const String& requestedClassName,
                      const Array& list,
                      bool includeSubclasses) {
  if (!list.empty()) {
    for (ArrayIter iter(list); iter; ++iter) {
      auto allowedClassName = iter.second().toString();
      auto const matches = includeSubclasses
        ? HHVM_FN(is_a)(requestedClassName, allowedClassName, true)
        : allowedClassName.get()->isame(requestedClassName.get());
      if (matches) return true;
    }
  }
  return false;
}
}

const StaticString s_throw("throw");
const StaticString s_allowed_classes("allowed_classes");
const StaticString s_include_subclasses("include_subclasses");

bool VariableUnserializer::whitelistCheck(const String& clsName) const {
  if (m_type != Type::Serialize || m_options.isNull()) {
    return true;
  }

  // PHP7-style class whitelisting
  // Allowed classes are allowed,
  // all others result in __Incomplete_PHP_Class
  if (m_options.exists(s_allowed_classes)) {
    auto allowed_classes = m_options[s_allowed_classes];
    auto const ok = [&] {
      if (allowed_classes.isArray()) {
        auto const subs = m_options[s_include_subclasses].toBoolean();
        return isWhitelistClass(clsName,
                                allowed_classes.toArray(),
                                subs);
      } else if (allowed_classes.isBoolean()) {
        return allowed_classes.toBoolean();
      } else {
        throw InvalidAllowedClassesException();
      }
    }();

    if (!ok && m_options[s_throw].toBoolean()) {
      throw_object(m_options[s_throw].toString(),
                   make_vec_array(clsName));
    }
    return ok;
  }

  if (!RuntimeOption::UnserializationWhitelistCheck) {
    // No need for BC HHVM-style whitelist check,
    // since the check isn't enabled.
    // Go with PHP5 default behavior of allowing all
    return true;
  }

  // Check for old-style whitelist
  if (isWhitelistClass(clsName, m_options, false)) {
    return true;
  }

  // Non-whitelisted class with a check enabled,
  // are we willing to hard-error over it?
  const char* err_msg =
    "The object being unserialized with class name '%s' "
    "is not in the given whitelist"; // followed by ' in <filename> on line %d'.

  if (RuntimeOption::UnserializationWhitelistCheckWarningOnly) {
    // Nope, just whine to the user and let it through
    raise_warning(err_msg, clsName.c_str());
    return true;
  } else {
    // Yes, shut it down.
    raise_error(err_msg, clsName.c_str());
    return false;
  }
}

void VariableUnserializer::addSleepingObject(const Object& o) {
  m_sleepingObjects.emplace_back(o);
}

bool VariableUnserializer::matchString(folly::StringPiece str) {
  const char* p = m_buf;
  assertx(p <= m_end);
  int total = 0;
  if (*p == 'S' && type() == VariableUnserializer::Type::APCSerialize) {
    total = 2 + 8 + 1;
    if (p + total > m_end) return false;
    p++;
    if (*p++ != ':') return false;
    auto const sd = *reinterpret_cast<StringData*const*>(p);
    assertx(sd->isStatic());
    if (str.compare(sd->slice()) != 0) return false;
    p += size_t(8);
  } else {
    const auto ss = str.size();
    if (ss >= 100) return false;
    int digits = ss >= 10 ? 2 : 1;
    total = 2 + digits + 2 + ss + 2;
    if (p + total > m_end) return false;
    if (*p++ != 's') return false;
    if (*p++ != ':') return false;
    if (digits == 2) {
      if (*p++ != '0' + ss/10) return false;
      if (*p++ != '0' + ss%10) return false;
    } else {
      if (*p++ != '0' + ss) return false;
    }
    if (*p++ != ':') return false;
    if (*p++ != '\"') return false;
    if (memcmp(p, str.data(), ss)) return false;
    p += ss;
    if (*p++ != '\"') return false;
  }
  if (*p++ != ';') return false;
  assertx(m_buf + total == p);
  m_buf = p;
  return true;
}

///////////////////////////////////////////////////////////////////////////////

// remainingProps should include the current property being unserialized.
void VariableUnserializer::unserializePropertyValue(tv_lval v,
                                                    int remainingProps) {
  assertx(remainingProps > 0);
  unserializeVariant(v);
  if (--remainingProps > 0) {
    auto lastChar = peekBack();
    if (lastChar != ';' && lastChar != '}') {
      throwUnterminatedProperty();
    }
  }
}

// nProp should include the current property being unserialized.
NEVER_INLINE
void VariableUnserializer::unserializeProp(ObjectData* obj,
                                           const String& key,
                                           Class* ctx,
                                           const String& realKey,
                                           int nProp) {

  auto const cls = obj->getVMClass();
  auto const lookup = cls->getDeclPropSlot(ctx, key.get());
  auto const slot = lookup.slot;
  tv_lval t;

  if (slot == kInvalidSlot || !lookup.accessible) {
    // Unserialize as a dynamic property. If this is the first, we need to
    // pre-allocate space in the array to ensure the elements don't move during
    // unserialization.
    t = obj->makeDynProp(realKey.get());
  } else {
    // We'll check if this doesn't violate the type-hint once we're done
    // unserializing all the props.
    t = obj->getPropLval(ctx, key.get());
  }

  unserializePropertyValue(t, nProp);
  if (!RuntimeOption::RepoAuthoritative) return;
  if (!Repo::get().global().HardPrivatePropInference) return;

  /*
   * We assume for performance reasons in repo authoriative mode that
   * we can see all the sets to private properties in a class.
   *
   * It's a hole in this if we don't check unserialization doesn't
   * violate what we've seen, which we handle by throwing if the repo
   * was built with this option.
   */
  if (UNLIKELY(slot == kInvalidSlot)) return;
  auto const repoTy = cls->declPropRepoAuthType(slot);
  if (LIKELY(tvMatchesRepoAuthType(*t, repoTy))) return;
  if (t.type() == KindOfUninit &&
      (cls->declProperties()[slot].attrs & AttrLateInit)) {
    return;
  }
  throwUnexpectedType(key, obj, *t);
}


NEVER_INLINE
void VariableUnserializer::unserializeRemainingProps(
  Object& obj,
  int remainingProps,
  Variant& serializedNativeData,
  bool& hasSerializedNativeData) {
  obj->unlockObject();
  SCOPE_EXIT { obj->lockObject(); };
  while (remainingProps > 0) {
    /*
      use the number of properties remaining as an estimate for
      the total number of dynamic properties when we see the
      first dynamic prop.  see getVariantPtr
    */
    Variant v;
    unserializeVariant(v.asTypedValue(), UnserializeMode::Key);
    String key = v.toString();
    int ksize = key.size();
    const char *kdata = key.data();
    int subLen = 0;
    if (key == s_serializedNativeDataKey) {
      unserializePropertyValue(serializedNativeData.asTypedValue(),
                               remainingProps--);
      hasSerializedNativeData = true;
    } else if (kdata[0] == '\0') {
      if (UNLIKELY(!ksize)) {
        raise_error("Cannot access empty property");
      }
      // private or protected
      subLen = strlen(folly::launder(kdata) + 1) + 2;
      if (UNLIKELY(subLen >= ksize)) {
        if (subLen == ksize) {
          raise_error("Cannot access empty property");
        } else {
          throwMangledPrivateProperty();
        }
      }
      String k(kdata + subLen, ksize - subLen, CopyString);
      Class* ctx = (Class*)-1;
      if (kdata[1] != '*') {
        ctx = Unit::lookupClass(
          String(kdata + 1, subLen - 2, CopyString).get());
      }
      unserializeProp(obj.get(), k, ctx, key,
                      remainingProps--);
    } else {
      unserializeProp(obj.get(), key, nullptr, key,
                      remainingProps--);
    }
  }
}

namespace {

static const StaticString
  s_Vector("Vector"), s_HH_Vector("HH\\Vector"),
  s_Map("Map"), s_HH_Map("HH\\Map"),
  s_Set("Set"), s_HH_Set("HH\\Set"),
  s_Pair("Pair"), s_HH_Pair("HH\\Pair"),
  s_StableMap("StableMap");

/*
 * For namespaced collections, returns an "alternate" name, which is a
 * collection name with or without the namespace qualifier, depending on
 * what's passed.
 * If no alternate name is found, returns nullptr.
 */
const StringData* getAlternateCollectionName(const StringData* clsName) {
  typedef hphp_hash_map<const StringData*, const StringData*,
                        string_data_hash, string_data_isame> ClsNameMap;

  auto getAltMap = [] {
    typedef std::pair<StaticString, StaticString> SStringPair;

    static ClsNameMap m;

    static std::vector<SStringPair> mappings {
      std::make_pair(s_Vector, s_HH_Vector),
      std::make_pair(s_Map, s_HH_Map),
      std::make_pair(s_Set, s_HH_Set),
      std::make_pair(s_Pair, s_HH_Pair)
    };

    for (const auto& p : mappings) {
      m[p.first.get()] = p.second.get();
      m[p.second.get()] = p.first.get();
    }

    // As part of StableMap merging into Map, StableMap is an alias for HH\\Map,
    // but Map is the sole alias for HH\\Map
    m[s_StableMap.get()] = s_HH_Map.get();
    return &m;
  };

  static const ClsNameMap* altMap = getAltMap();

  auto it = altMap->find(clsName);
  return it != altMap->end() ? it->second : nullptr;
}

Class* tryAlternateCollectionClass(const StringData* clsName) {
  auto altName = getAlternateCollectionName(clsName);
  return altName ? Unit::getClass(altName, /* autoload */ false) : nullptr;
}

/*
 * Try to read 'str' while advancing 'cur' without reaching 'end'.
 */
ALWAYS_INLINE
static bool match(const char*& cur,
                  const char* expected,
                  const char* const end) {
  if (cur + strlen(expected) >= end) return false;
  while (*expected) {
    if (*cur++ != *expected++) return false;
  }
  return true;
}

ALWAYS_INLINE
static int64_t read64(const char*& cur) {
  auto p = hh_strtoll_base10(cur);
  cur = p.second;
  return p.first;
}

/*
 * Read an int64 from 'cur' into 'out'. Returns false on unexpected
 * (but possibly still legal) format or if 'end' is reached.
 */
ALWAYS_INLINE
bool readInt64(const char*& cur, const char* const end, int64_t& out) {
  if (!match(cur, "i:", end)) return false;
  out = read64(cur);
  return match(cur, ";", end);
}

/*
 * Read, allocate, and return a string from 'cur'. Returns null on unexpected
 * (but possibly still legal) format or if 'end' is reached, without allocating.
 */
ALWAYS_INLINE
static StringData* readStringData(const char*& cur, const char* const end,
                                  int maxLen) {
  if (!match(cur, "s:", end)) return nullptr;
  auto len = read64(cur);
  if (len < 0 || len >= maxLen) return nullptr;
  if (!match(cur, ":\"", end)) return nullptr;
  auto const slice = folly::StringPiece(cur, len);
  if ((cur += len) >= end) return nullptr;
  if (!match(cur, "\";", end)) return nullptr;
  // TODO(11398853): Consider streaming/non-temporal stores here.
  auto sd = StringData::Make(slice, CopyString);
  return sd;
}
}

NEVER_INLINE
void VariableUnserializer::unserializeVariant(
    tv_lval self,
    UnserializeMode mode /* = UnserializeMode::Value */) {

  // If we're overwriting an array element or property value, save the old
  // value in case it's later referenced via an r: or R: ref.
  if (isRefcountedType(self.type()) && mode == UnserializeMode::Value) {
    m_overwrittenList.append(*self);
  }

  // NOTE: If you make changes to how serialization and unserialization work,
  // make sure to update reserialize() here and test_apc_reserialize()
  // in "test/ext/test_ext_apc.cpp".

  char type = readChar();
  char sep = readChar();

  if (type != 'R') {
    add(self, mode);
  }

  if (type == 'N') {
    if (sep != ';') throwUnexpectedSep(';', sep);
    tvSetNull(self); // NULL *IS* the value, without we get undefined warnings
    return;
  }
  if (sep != ':') throwUnexpectedSep(':', sep);

  switch (type) {
  case 'r':
  case 'R':
    {
      int64_t id = readInt();
      tvSet(getByVal(id), self);
    }
    break;
  case 'b':
    {
      int64_t v = readInt();
      tvSetBool((bool)v, self);
      break;
    }
  case 'i':
    {
      int64_t v = readInt();
      tvSetInt(v, self);
      break;
    }
  case 'd':
    {
      char ch = peek();
      bool negative = false;
      if (ch == '-') {
        negative = true;
        readChar();
        ch = peek();
      }
      double v;
      if (ch == 'I') {
        auto str = readStr(3);
        if (str.size() != 3 || str[1] != 'N' || str[2] != 'F') {
          throwUnexpectedStr("INF", str);
        }
        v = std::numeric_limits<double>::infinity();
      } else if (ch == 'N') {
        auto str = readStr(3);
        if (str.size() != 3 || str[1] != 'A' || str[2] != 'N') {
          throwUnexpectedStr("NAN", str);
        }
        v = std::numeric_limits<double>::quiet_NaN();
      } else {
        v = readDouble();
      }
      tvSetDouble(negative ? -v : v, self);
    }
    break;
  case 's':
    {
      String v = unserializeString();
      tvMove(make_tv<KindOfString>(v.detach()), self);
      if (!endOfBuffer()) {
        // Semicolon *should* always be required,
        // but PHP's implementation allows omitting it
        // and still functioning.
        // Worse, it throws it away without any check.
        // So we'll do the same.  Sigh.
        readChar();
      }
    }
    return;
  case 'S':
    if (this->type() == VariableUnserializer::Type::APCSerialize) {
      auto str = readStr(8);
      assertx(str.size() == 8);
      auto const sd = *reinterpret_cast<StringData*const*>(&str[0]);
      assertx(sd->isStatic());
      tvMove(make_tv<KindOfPersistentString>(sd), self);
    } else {
      throwUnknownType(type);
    }
    break;
  case 'a': // PHP array
  case 'D': // Dict
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      // It seems silly to check this here, but GCC actually generates much
      // better code this way.
      auto a = (type == 'a') ?
        unserializeArray() :
        unserializeDict();
      if (UNLIKELY(m_markLegacyArrays && type == 'a')) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'x': // legacy dict
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeDict();
      a.setLegacyArray(true);
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'Y': // DArray
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeDArray();
      if (UNLIKELY(m_markLegacyArrays)) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'y': // VArray
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeVArray();
      if (UNLIKELY(m_markLegacyArrays)) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'v': // Vec
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeVec();
      tvMove(make_tv<KindOfVec>(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'X': // legacy vec
  {
    // Check stack depth to avoid overflow.
    check_recursion_throw();
    auto a = unserializeVec();
    a.setLegacyArray(true);
    tvMove(make_tv<KindOfVec>(a.detach()), self);
  }
  return; // array has '}' terminating
  case 'k': // Keyset
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeKeyset();
      tvMove(make_tv<KindOfKeyset>(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'L':
    {
      int64_t id = readInt();
      expectChar(':');
      String rsrcName = unserializeString();
      expectChar('{');
      expectChar('}');
      auto rsrc = req::make<DummyResource>();
      rsrc->o_setResourceId(id);
      rsrc->m_class_name = std::move(rsrcName);
      tvMove(make_tv<KindOfResource>(rsrc.detach()->hdr()), self);
    }
    return; // resource has '}' terminating
  case 'O':
  case 'V':
  case 'K':
    {
      String clsName = unserializeString();

      expectChar(':');
      const int64_t size = readInt();
      expectChar(':');
      expectChar('{');

      const bool allowObjectFormatForCollections = true;

      Class* cls = nullptr;

      // If we are potentially dealing with a collection, we need to try to
      // load the collection class under an alternate name so that we can
      // deserialize data that was serialized before the migration of
      // collections to the HH namespace.

      if (type == 'O') {
        if (whitelistCheck(clsName)) {
          if (allowObjectFormatForCollections) {
            // In order to support the legacy {O|V}:{Set|Vector|Map}
            // serialization, we defer autoloading until we know that there's
            // no alternate (builtin) collection class.
            cls = Unit::getClass(clsName.get(), /* autoload */ false);
            if (!cls) {
              cls = tryAlternateCollectionClass(clsName.get());
            }
          }

          // No valid class was found, lets try the autoloader.
          if (!cls) {
            if (!is_valid_class_name(clsName.slice())) {
              throwInvalidClassName();
            }
            cls = Unit::loadClass(clsName.get()); // with autoloading
          }
        }
      } else {
        // Collections are CPP builtins; don't attempt to autoload
        cls = Unit::getClass(clsName.get(), /* autoload */ false);
        if (!cls) {
          cls = tryAlternateCollectionClass(clsName.get());
        }
        if (!cls || !cls->isCollectionClass()) {
          throwNotCollection(clsName);
        }
      }

      Object obj;
      auto remainingProps = size;
      if (cls) {
        // Only unserialize CPP extension types which can actually support
        // it. Otherwise, we risk creating a CPP object without having it
        // initialized completely.
        if (cls->instanceCtor() && !cls->isCppSerializable() &&
            !cls->isCollectionClass()) {
          assertx(obj.isNull());
          throw_null_pointer_exception();
        } else {
          if (UNLIKELY(collections::isType(cls, CollectionType::Pair))) {
            if (UNLIKELY(size != 2)) {
              throwInvalidPair();
            }
            // pairs can't be constructed without elements
            obj = Object{req::make<c_Pair>(make_tv<KindOfNull>(),
                                           make_tv<KindOfNull>(),
                                           c_Pair::NoIncRef{})};
          } else if (UNLIKELY(cls->hasReifiedGenerics())) {
            // First prop on the serialized list is the reified generics prop
            if (!matchString(s_86reified_prop.slice())) {
              throwInvalidOFormat(clsName);
            }
            TypedValue tv = make_tv<KindOfNull>();
            auto const t = tv_lval{&tv};
            unserializePropertyValue(t, remainingProps--);
            if (!TypeStructure::coerceToTypeStructureList_SERDE_ONLY(t)) {
              throwInvalidOFormat(clsName);
            }
            assertx(tvIsHAMSafeVArray(t));
            obj = Object{cls, t.val().parr};
          } else {
            obj = Object{cls};
          }
        }
      } else {
        warnOrThrowUnknownClass(clsName);
        obj = Object{SystemLib::s___PHP_Incomplete_ClassClass};
        obj->setProp(nullptr, s_PHP_Incomplete_Class_Name.get(),
                     clsName.asTypedValue());
      }
      assertx(!obj.isNull());
      tvSet(make_tv<KindOfObject>(obj.get()), self);

      if (remainingProps > 0) {
        // Check stack depth to avoid overflow.
        check_recursion_throw();

        if (type == 'O') {
          // Collections are not allowed
          if (obj->isCollection()) {
            throwInvalidOFormat(clsName);
          }

          Variant serializedNativeData = init_null();
          bool hasSerializedNativeData = false;
          bool checkRepoAuthType =
            RuntimeOption::RepoAuthoritative &&
            Repo::get().global().HardPrivatePropInference;
          Class* objCls = obj->getVMClass();
          // Try fast case.
          if (remainingProps >= objCls->numDeclProperties() -
                                (objCls->hasReifiedGenerics() ? 1 : 0)) {
            auto mismatch = false;
            auto const objProps = obj->props();

            auto const declProps = objCls->declProperties();
            for (auto const& p : declProps) {
              auto slot = p.serializationIdx;
              auto index = objCls->propSlotToIndex(slot);
              auto const& prop = declProps[slot];
              if (prop.name == s_86reified_prop.get()) continue;
              if (!matchString(prop.mangledName->slice())) {
                mismatch = true;
                break;
              }

              // don't need to worry about overwritten list, because
              // this is definitely the first time we're setting this
              // property.
              auto const t = objProps->at(index);
              unserializePropertyValue(t, remainingProps--);

              if (UNLIKELY(checkRepoAuthType &&
                           !tvMatchesRepoAuthType(*t, prop.repoAuthType))) {
                throwUnexpectedType(prop.name, obj.get(), *t);
              }
            }
            // If everything matched, all remaining properties are dynamic.
            if (!mismatch && remainingProps > 0) {
              // the dynPropTable can be mutated while we're deserializing
              // the contents of this object's prop array. Don't hold a
              // reference to this object's entry in the table while looping.
              obj->reserveDynProps(remainingProps);
              while (remainingProps > 0) {
                Variant v;
                unserializeVariant(v.asTypedValue(), UnserializeMode::Key);
                String key = v.toString();
                if (key == s_serializedNativeDataKey) {
                  unserializePropertyValue(serializedNativeData.asTypedValue(),
                                           remainingProps--);
                  hasSerializedNativeData = true;
                } else {
                  auto kdata = key.data();
                  if (kdata[0] == '\0') {
                    auto ksize = key.size();
                    if (UNLIKELY(ksize == 0)) {
                      raise_error("Cannot access empty property");
                    }
                    // private or protected
                    auto subLen = strlen(folly::launder(kdata) + 1) + 2;
                    if (UNLIKELY(subLen >= ksize)) {
                      if (subLen == ksize) {
                        raise_error("Cannot access empty property");
                      } else {
                        throwMangledPrivateProperty();
                      }
                    }
                  }
                  auto const lval = obj->makeDynProp(key.get());
                  unserializePropertyValue(lval, remainingProps--);
                }
              }
            }
          }
          if (remainingProps > 0) {
            INC_TPC(unser_prop_slow);
            unserializeRemainingProps(obj, remainingProps,
                                      serializedNativeData,
                                      hasSerializedNativeData);
            remainingProps = 0;
          } else {
            INC_TPC(unser_prop_fast);
          }

          // Verify that all the unserialized properties satisfy their
          // type-hints. Its safe to do it like this (after we've set the values
          // in the properties) because this object hasn't escaped to the
          // outside world yet.
          obj->verifyPropTypeHints();

          // nativeDataWakeup is called last to ensure that all properties are
          // already unserialized. We also ensure that nativeDataWakeup is
          // invoked regardless of whether or not serialized native data exists
          // within the serialized content.
          if (obj->hasNativeData() &&
              obj->getVMClass()->getNativeDataInfo()->isSerializable()) {
            Native::nativeDataWakeup(obj.get(), serializedNativeData);
          } else if (hasSerializedNativeData) {
            raise_warning("%s does not expect any serialized native data.",
                          clsName.data());
          }
        } else {
          assertx(type == 'V' || type == 'K');
          if (!obj->isCollection()) {
            throwNotCollection(clsName);
          }
          unserializeCollection(obj.get(), size, type);
        }
      }
      expectChar('}');

      if (cls &&
          cls->lookupMethod(s___wakeup.get()) &&
          (this->type() != VariableUnserializer::Type::DebuggerSerialize ||
           (cls->instanceCtor() && cls->isCppSerializable()))) {
        // Don't call wakeup when unserializing for the debugger, except for
        // natively implemented classes.
        addSleepingObject(obj);
      }

      check_non_safepoint_surprise();
    }
    return; // object has '}' terminating
  case 'C':
    {
      if (this->type() == VariableUnserializer::Type::DebuggerSerialize) {
        raise_error("Debugger shouldn't call custom unserialize method");
      }
      String clsName = unserializeString();

      expectChar(':');
      String serialized = unserializeString('{', '}');

      auto obj = [&]() -> Object {
        if (whitelistCheck(clsName)) {
          // Try loading without the autoloader first
          auto cls = Unit::getClass(clsName.get(), /* autoload */ false);
          if (!cls) {
            if (!is_valid_class_name(clsName.slice())) {
              throwInvalidClassName();
            }
            cls = Unit::loadClass(clsName.get());
          }
          if (cls) {
            return Object::attach(g_context->createObject(cls, init_null_variant,
                                                          false /* init */));
          }
        }
        if (!allowUnknownSerializableClass()) {
          raise_error("unknown class %s", clsName.data());
        }
        warnOrThrowUnknownClass(clsName);
        Object ret = create_object_only(s_PHP_Incomplete_Class);
        ret->setProp(nullptr, s_PHP_Incomplete_Class_Name.get(),
                     clsName.asTypedValue());
        ret->setProp(nullptr, s_serialized.get(), serialized.asTypedValue());
        return ret;
      }();

      if (!obj->instanceof(SystemLib::s_SerializableClass)) {
        raise_warning("Class %s has no unserializer",
                      obj->getClassName().data());
      } else {
        obj->o_invoke_few_args(s_unserialize, 1, serialized);
      }

      tvMove(make_tv<KindOfObject>(obj.detach()), self);
    }
    return; // object has '}' terminating
  default:
    throwUnknownType(type);
  }
  expectChar(';');
}

Array VariableUnserializer::unserializeArray() {
  int64_t size = readInt();
  expectChar(':');
  expectChar('{');
  if (size == 0) {
    expectChar('}');
    return m_forceDArrays || type() == Type::Serialize
      ? Array::CreateDArray()
      : Array::Create();
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }
  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  // Pre-allocate an ArrayData of the given size, to avoid escalation in the
  // middle, which breaks references.
  auto arr = m_forceDArrays || type() == Type::Serialize
    ? DArrayInit(size).toArray()
    : MixedArrayInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  return arr;
}

arrprov::Tag VariableUnserializer::unserializeProvenanceTag() {
  auto const finish = [&] (auto tag) -> arrprov::Tag {
    if (!RuntimeOption::EvalArrayProvenance) return {};
    return tag;
  };

  auto const read_filename = [&]() -> const StringData* {
    if (peek() == 't') {
      assertx(m_unitFilename);
      expectChar('t');
      return m_unitFilename;
    } else {
      expectChar('s');
      expectChar(':');
      return makeStaticString(unserializeString().get());
    }
  };

  if (type() != VariableUnserializer::Type::Internal) return {};
  if (peek() == 'p') {
    expectChar('p');
    expectChar(':');
    expectChar('i');
    expectChar(':');
    auto const line = static_cast<int>(readInt());
    expectChar(';');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag { filename, line }
    );
  } else if (peek() == 'P') {
    expectChar('P');
    expectChar(';');
    return finish(
      arrprov::Tag::RepoUnion()
    );
  } else if (peek() == 'r') {
    expectChar('r');
    expectChar(':');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag::TraitMerge(filename)
    );
  } else if (peek() == 'e') {
    expectChar('e');
    expectChar(':');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag::LargeEnum(filename)
    );
  } else {
    return {};
  }
}

Array VariableUnserializer::unserializeDict() {
  if (m_dvOverrides) m_dvOverrides->push_back(false);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvHackArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyDictArray(), provTag)
      : staticEmptyDictArray()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  Array arr = DictInit(size).toArray();
  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeVec() {
  if (m_dvOverrides) m_dvOverrides->push_back(false);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvHackArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyVec(), provTag)
      : staticEmptyVec()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }
  auto const sizeClass = PackedArray::capacityToSizeIndex(size);
  auto const allocsz = MemoryManager::sizeIndex2Size(sizeClass);

  // For large arrays, do a naive pre-check for OOM.
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  Array arr = VecInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    unserializeVariant(PackedArray::LvalNewInPlace(arr.get()));
    if (i < size - 1) checkElemTermination();
  }
  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeVArray() {
  if (m_dvOverrides) m_dvOverrides->push_back(true);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvDVArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    if (m_type != Type::Serialize) {
      return Array::attach(provTag
        ? arrprov::tagStaticArr(staticEmptyVArray(), provTag)
        : staticEmptyVArray()
      );
    }
    return m_forceDArrays
      ? Array::attach(provTag
          ? arrprov::tagStaticArr(staticEmptyDArray(), provTag)
          : staticEmptyDArray()
        )
      : Array::attach(provTag
          ? arrprov::tagStaticArr(staticEmptyVArray(), provTag)
          : staticEmptyVArray()
        );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  auto const oomCheck = [&](size_t allocsz) {
    // For large arrays, do a naive pre-check for OOM.
    if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
      check_non_safepoint_surprise();
    }
  };

  auto arr = Array{};
  if (m_forceDArrays && m_type == Type::Serialize) {
    // Deserialize to vector-ish darray. Use direct calls to MixedArray.
    oomCheck(MixedArray::computeAllocBytesFromMaxElms(size));

    arr = DArrayInit(size).toArray();
    reserveForAdd(size);

    for (int64_t i = 0; i < size; i++) {
      unserializeVariant(MixedArray::LvalInPlace(arr.get(), i));
      if (i < size - 1) checkElemTermination();
    }
  } else {
    // Deserialize to varray. Use direct calls to MixedArray.
    auto const index = PackedArray::capacityToSizeIndex(size);
    oomCheck(MemoryManager::sizeIndex2Size(index));

    arr = VArrayInit(size).toArray();
    reserveForAdd(size);

    for (int64_t i = 0; i < size; i++) {
      unserializeVariant(PackedArray::LvalNewInPlace(arr.get()));
      if (i < size - 1) checkElemTermination();
    }
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeDArray() {
  if (m_dvOverrides) m_dvOverrides->push_back(true);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvDVArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyDArray(), provTag)
      : staticEmptyDArray()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto arr = DArrayInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeKeyset() {
  int64_t size = readInt();
  expectChar(':');
  expectChar('{');
  if (size == 0) {
    expectChar('}');
    return Array::CreateKeyset();
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = SetArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  KeysetInit init(size);
  for (int64_t i = 0; i < size; i++) {
    Variant key;
    // Use key mode to stop the unserializer from keeping a pointer to this
    // variant (since its stack-allocated).
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);

    auto const type = key.getType();
    if (UNLIKELY(!isStringType(type) && !isIntType(type))) {
      throwKeysetValue();
    }

    init.add(key);

    if (i < (size - 1)) {
      auto lastChar = peekBack();
      if ((lastChar != ';' && lastChar != '}')) {
        throwUnterminatedElement();
      }
    }
  }
  check_non_safepoint_surprise();
  expectChar('}');
  return init.toArray();
}


folly::StringPiece
VariableUnserializer::unserializeStringPiece(char delimiter0, char delimiter1) {
  int64_t size = readInt();
  if (size >= RuntimeOption::MaxSerializedStringSize) {
    throwLargeStringSize(size);
  }
  if (size < 0) {
    throwNegativeStringSize(size);
  }
  expectChar(':');
  expectChar(delimiter0);
  auto const piece = readStr(size);
  expectChar(delimiter1);
  return piece;
}

String VariableUnserializer::unserializeString(char delimiter0,
                                               char delimiter1) {
  auto const piece = unserializeStringPiece(delimiter0, delimiter1);
  return String::attach(readOnly() ?
                        makeStaticString(piece) :
                        StringData::Make(piece, CopyString));
}

void VariableUnserializer::unserializeCollection(ObjectData* obj, int64_t sz,
                                                 char type) {
  switch (obj->collectionType()) {
    case CollectionType::Pair:
      unserializePair(obj, sz, type);
      break;
    case CollectionType::Vector:
    case CollectionType::ImmVector:
      unserializeVector(obj, sz, type);
      break;
    case CollectionType::Map:
    case CollectionType::ImmMap:
      unserializeMap(obj, sz, type);
      break;
    case CollectionType::Set:
    case CollectionType::ImmSet:
      unserializeSet(obj, sz, type);
      break;
  }
}

void VariableUnserializer::unserializeVector(ObjectData* obj, int64_t sz,
                                             char type) {
  if (type != 'V') throwBadFormat(obj, type);

  auto const sizeClass = PackedArray::capacityToSizeIndex(sz);
  auto const allocsz = MemoryManager::sizeIndex2Size(sizeClass);
  // For large vectors, do a naive pre-check for OOM.
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto bvec = static_cast<BaseVector*>(obj);
  bvec->reserve(sz);
  reserveForAdd(sz);
  for (int64_t i = 0; i < sz; ++i) {
    auto tv = bvec->appendForUnserialize(i);
    HPHP::type(tv) = KindOfNull;
    unserializeVariant(tv);
  }
}

/*
 * Attempts to unserialize into an initially empty HH\Map of string->int/string.
 * Returns false and leaves both 'map' and 'uns' untouched on failure, including
 * unexpected types and possibly legal, but uncommon, encodings.
 */
NEVER_INLINE
bool VariableUnserializer::tryUnserializeStrIntMap(BaseMap* map, int64_t sz) {
  auto b = head();
  /*
   * For efficiency, we don't add the keys/values to m_refs, so don't support
   * back-references appearing after this point. For simplicity, we thus require
   * this map to be the root object being unserialized.
   */
  if (folly::StringPiece(begin(), b) !=
      folly::to<std::string>("K:6:\"HH\\Map\":", sz, ":{")) {
    return false;
  }
  auto const end = this->end();
  auto const maxKeyLen = RuntimeOption::MaxSerializedStringSize;
  /*
   * First, parse the entire input and allocate the keys (accessing lots of
   * data, but mostly sequentially).
   */
  auto checkPoint = map->batchInsertBegin(sz);
  int64_t i = 0;
  for (; i < sz; ++i) {
    auto sd = readStringData(b, end, maxKeyLen);
    if (!sd) break;
    String key = String::attach(sd);
    auto tv = map->batchInsert(key.get());
    tv->m_type = KindOfNull;
    if (*b == 'i') {
      if (!readInt64(b, end, tv->m_data.num)) break;
      tv->m_type = KindOfInt64;
    } else if (*b == 's') {
      auto sd = readStringData(b, end, maxKeyLen);
      if (!sd) break;
      tv->m_data.pstr = sd;
      tv->m_type = KindOfString;
    } else {
      break;
    }
  }
  /*
   * On success, finalize the hash table insertion (very random access).
   */
  if (i == sz && map->tryBatchInsertEnd(checkPoint)) {
    set(b, end);
    return true;
  }
  map->batchInsertAbort(checkPoint);
  return false;
}

void VariableUnserializer::unserializeMap(ObjectData* obj, int64_t sz,
                                          char type) {
  if (type != 'K') throwBadFormat(obj, type);

  // For large maps, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(sz);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto map = static_cast<BaseMap*>(obj);
  map->reserve(sz);
  if (sz >= RuntimeOption::UnserializationBigMapThreshold &&
      tryUnserializeStrIntMap(map, sz)) {
    return;
  }

  reserveForAdd(sz + sz); // keys + values
  for (int64_t i = 0; i < sz; ++i) {
    Variant k;
    unserializeVariant(k.asTypedValue(), UnserializeMode::ColKey);
    TypedValue* tv = nullptr;
    if (k.isInteger()) {
      auto h = k.toInt64();
      tv = map->findForUnserialize(h);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) {
        tv = k.asTypedValue();
        goto do_unserialize;
      }
    } else if (k.isString()) {
      auto key = k.getStringData();
      tv = map->findForUnserialize(key);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) {
        tv = k.asTypedValue();
        goto do_unserialize;
      }
    } else {
      throwInvalidKey();
    }
    tv->m_type = KindOfNull;
do_unserialize:
    unserializeVariant(tv);
  }
}

void VariableUnserializer::unserializeSet(ObjectData* obj, int64_t sz,
                                          char type) {
  if (type != 'V') throwBadFormat(obj, type);

  // For large maps, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(sz);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto set = static_cast<BaseSet*>(obj);
  set->reserve(sz);

  reserveForAdd(sz);
  for (int64_t i = 0; i < sz; ++i) {
    // When unserializing an element of a Set, we use Mode::ColKey for now.
    // This will make the unserializer to reserve an id for the element
    // but won't allow referencing the element via 'r' or 'R'.
    Variant k;
    unserializeVariant(k.asTypedValue(), UnserializeMode::ColKey);
    if (k.isInteger()) {
      auto h = k.toInt64();
      auto tv = set->findForUnserialize(h);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) continue;
      tv->m_type = KindOfInt64;
      tv->m_data.num = h;
    } else if (k.isString()) {
      auto key = k.getStringData();
      auto tv = set->findForUnserialize(key);
      if (UNLIKELY(!tv)) continue;
      // This increments the string's refcount twice, once for
      // the key and once for the value
      tvDup(make_tv<KindOfString>(key), *tv);
    } else {
      throwInvalidHashKey(obj);
    }
  }
}

void VariableUnserializer::unserializePair(ObjectData* obj, int64_t sz,
                                           char type) {
  assertx(sz == 2);
  if (type != 'V') throwBadFormat(obj, type);
  auto pair = static_cast<c_Pair*>(obj);
  unserializeVariant(pair->at(0));
  unserializeVariant(pair->at(1));
}

////////////////////////////////////////////////////////////////////////////////

void VariableUnserializer::reserialize(StringBuffer& buf) {

  char type = readChar();
  char sep = readChar();

  if (type == 'N') {
    buf.append(type);
    buf.append(sep);
    return;
  }

  switch (type) {
  case 'r':
  case 'R':
  case 'b':
  case 'i':
  case 'd':
    {
      buf.append(type);
      buf.append(sep);
      while (peek() != ';') {
        char ch;
        ch = readChar();
        buf.append(ch);
      }
    }
    break;
  case 'S':
  case 'A':
    {
      // shouldn't happen, but keep the code here anyway.
      buf.append(type);
      buf.append(sep);
      auto str = readStr(8);
      buf.append(str.data(), str.size());
    }
    break;
  case 's':
    {
      String v = unserializeString();
      assertx(!v.isNull());
      if (v.get()->isStatic()) {
        union {
          char pointer[8];
          StringData *sd;
        } u;
        u.sd = v.get();
        buf.append("S:");
        buf.append(u.pointer, 8);
        buf.append(';');
      } else {
        buf.append("s:");
        buf.append(v.size());
        buf.append(":\"");
        buf.append(v.data(), v.size());
        buf.append("\";");
      }
      sep = readChar();
      return;
    }
    break;
  case 'a':
  case 'D':
  case 'Y':
  case 'H':
    {
      buf.append(type == 'a' ? "a:" : (type == 'Y' ? "Y:" :
            (type == 'D' ? "D:" : "H:")));
      int64_t size = readInt();
      char sep2 = readChar();
      buf.append(size);
      buf.append(sep2);
      sep2 = readChar();
      buf.append(sep2);
      for (int64_t i = 0; i < size; i++) {
        reserialize(buf); // key
        reserialize(buf); // value
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
    break;
  case 'v':
  case 'k':
  case 'y':
    {
      buf.append(type == 'v' ? "v:" : (type == 'y' ? "y:" : "k:"));
      int64_t size = readInt();
      char sep2 = readChar();
      buf.append(size);
      buf.append(sep2);
      sep2 = readChar();
      buf.append(sep2);
      for (int64_t i = 0; i < size; ++i) {
        reserialize(buf);
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
  case 'o':
  case 'O':
  case 'V':
  case 'K':
    {
      buf.append(type);
      buf.append(sep);

      auto const clsName = unserializeStringPiece();
      buf.append(static_cast<int>(clsName.size()));
      buf.append(":\"");
      buf.append(clsName.data(), clsName.size());
      buf.append("\":");

      readChar();
      int64_t size = readInt();
      char sep2 = readChar();

      buf.append(size);
      buf.append(sep2);
      sep2 = readChar(); // '{'
      buf.append(sep2);
      // 'V' type is a series with values only, while all other
      // types are series with keys and values
      int64_t i = type == 'V' ? size : size * 2;
      while (i--) {
        reserialize(buf);
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
    break;
  case 'C':
    {
      buf.append(type);
      buf.append(sep);

      auto const clsName = unserializeStringPiece();
      buf.append(static_cast<int>(clsName.size()));
      buf.append(":\"");
      buf.append(clsName.data(), clsName.size());
      buf.append("\":");

      sep = readChar(); // ':'
      auto const serialized = unserializeStringPiece('{', '}');
      buf.append(static_cast<int>(serialized.size()));
      buf.append(":{");
      buf.append(serialized.data(), serialized.size());
      buf.append('}');
      return;
    }
    break;
  default:
    throwUnknownType(type);
  }

  sep = readChar(); // the last ';'
  buf.append(sep);
}

}


Fixed version (ground truth):
/*
   +----------------------------------------------------------------------+
   | 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-unserializer.h"

#include <algorithm>
#include <utility>

#include <folly/Conv.h>
#include <folly/Range.h>
#include <folly/lang/Launder.h>

#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/autoload-handler.h"
#include "hphp/runtime/base/set-array.h"
#include "hphp/runtime/base/collections.h"
#include "hphp/runtime/base/comparisons.h"
#include "hphp/runtime/base/dummy-resource.h"
#include "hphp/runtime/base/execution-context.h"
#include "hphp/runtime/base/runtime-error.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/struct-log-util.h"
#include "hphp/runtime/base/request-info.h"
#include "hphp/runtime/base/variable-serializer.h"

#include "hphp/runtime/ext/collections/ext_collections-map.h"
#include "hphp/runtime/ext/collections/ext_collections-pair.h"
#include "hphp/runtime/ext/collections/ext_collections-set.h"
#include "hphp/runtime/ext/collections/ext_collections-vector.h"
#include "hphp/runtime/ext/std/ext_std_classobj.h"

#include "hphp/runtime/vm/native-data.h"
#include "hphp/runtime/vm/repo-global-data.h"
#include "hphp/runtime/vm/repo.h"

#include "hphp/runtime/vm/jit/perf-counters.h"

#include "hphp/zend/zend-strtod.h"

namespace HPHP {

namespace {

enum class ArrayKind { PHP, Dict, Vec, Keyset };

[[noreturn]] NEVER_INLINE
void throwUnexpectedSep(char expect, char actual) {
  throw Exception("Expected '%c' but got '%c'", expect, actual);
}

[[noreturn]] NEVER_INLINE
void throwOutOfRange(int64_t id) {
  throw Exception("Id %" PRId64 " out of range", id);
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedStr(const char* expect, folly::StringPiece& actual) {
  throw Exception("Expected '%s' but got '%.*s'", expect,
                  (int)actual.size(), actual.data());
}

[[noreturn]] NEVER_INLINE
void throwUnknownType(char type) {
  throw Exception("Unknown type '%c'", type);
}

[[noreturn]] NEVER_INLINE
void throwInvalidPair() {
  throw Exception("Pair objects must have exactly 2 elements");
}

[[noreturn]] NEVER_INLINE
void throwInvalidOFormat(const String& clsName) {
  throw Exception("%s does not support the 'O' serialization format",
                  clsName.data());
}

[[noreturn]] NEVER_INLINE
void throwMangledPrivateProperty() {
  throw Exception("Mangled private object property");
}

[[noreturn]] NEVER_INLINE
void throwUnterminatedProperty() {
  throw Exception("Object property not terminated properly");
}

[[noreturn]] NEVER_INLINE
void throwNotCollection(const String& clsName) {
  throw Exception("%s is not a collection class", clsName.data());
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedType(const String& key, const ObjectData* obj,
                         TypedValue type) {
  auto msg = folly::format(
    "Property {} for class {} was deserialized with type ({}) that "
    "didn't match what we inferred in static analysis",
    key,
    obj->getVMClass()->name(),
    tname(type.m_type)
  ).str();
  throw Exception(msg);
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedType(const StringData* key, const ObjectData* obj,
                         TypedValue type) {
  String str(key->data(), key->size(), CopyString);
  throwUnexpectedType(str, obj, type);
}

[[noreturn]] NEVER_INLINE
void throwArraySizeOutOfBounds() {
  throw Exception("Array size out of bounds");
}

[[noreturn]] NEVER_INLINE
void throwInvalidKey() {
  throw Exception("Invalid key");
}

[[noreturn]] NEVER_INLINE
void throwUnterminatedElement() {
  throw Exception("Array element not terminated properly");
}

[[noreturn]] NEVER_INLINE
void throwLargeStringSize(int64_t size) {
  throw Exception("Size of serialized string (%" PRId64 ") exceeds max", size);
}

[[noreturn]] NEVER_INLINE
void throwNegativeStringSize(int64_t size) {
  throw Exception("Size of serialized string (%" PRId64 ") "
                  "must not be negative", size);
}

[[noreturn]] NEVER_INLINE
void throwBadFormat(const ObjectData* obj, char type) {
  throw Exception("%s does not support the '%c' serialization format",
                  header_names[(int)obj->headerKind()], type);
}

[[noreturn]] NEVER_INLINE
void throwInvalidHashKey(const ObjectData* obj) {
  throw Exception("%s values must be integers or strings",
                  header_names[(int)obj->headerKind()]);
}

[[noreturn]] NEVER_INLINE
void throwColRKey() {
  throw Exception("Referring to collection keys using the 'r' encoding "
                    "is not supported");
}

[[noreturn]] NEVER_INLINE
void throwColRefValue() {
  throw Exception("Collection values cannot be taken by reference");
}

[[noreturn]] NEVER_INLINE
void throwColRefKey() {
  throw Exception("Collection keys cannot be taken by reference");
}

[[noreturn]] NEVER_INLINE
void throwUnexpectedEOB() {
  throw Exception("Unexpected end of buffer during unserialization");
}

[[noreturn]] NEVER_INLINE
void throwVecRefValue() {
  throw Exception("Vecs cannot contain references");
}

[[noreturn]] NEVER_INLINE
void throwDictRefValue() {
  throw Exception("Dicts cannot contain references");
}

[[noreturn]] NEVER_INLINE
void throwKeysetValue() {
  throw Exception("Keysets can only contain integers and strings");
}

[[noreturn]] NEVER_INLINE
void throwInvalidClassName() {
  throw Exception("Provided class name is invalid");
}

void warnOrThrowUnknownClass(const String& clsName) {
  if (RuntimeOption::EvalForbidUnserializeIncompleteClass) {
    auto const msg = folly::sformat(
      "Attempted to unserialize class named '{}' but it doesn't exist",
      clsName.toCppString()
    );
    if (RuntimeOption::EvalForbidUnserializeIncompleteClass > 1) {
      throw_object("Exception", make_vec_array(msg));
    } else {
      raise_warning(msg);
    }
  }
}
}

const StaticString
  s_serialized("serialized"),
  s_unserialize("unserialize"),
  s_PHP_Incomplete_Class("__PHP_Incomplete_Class"),
  s_PHP_Incomplete_Class_Name("__PHP_Incomplete_Class_Name"),
  s___wakeup("__wakeup");

///////////////////////////////////////////////////////////////////////////////

const StaticString s_force_darrays{"force_darrays"};
const StaticString s_mark_legacy_arrays{"mark_legacy_arrays"};

VariableUnserializer::VariableUnserializer(
  const char* str,
  size_t len,
  Type type,
  bool allowUnknownSerializableClass,
  const Array& options)
    : m_type(type)
    , m_readOnly(false)
    , m_buf(str)
    , m_end(str + len)
    , m_unknownSerializable(allowUnknownSerializableClass)
    , m_options(options)
    , m_begin(str)
    , m_forceDArrays{m_options[s_force_darrays].toBoolean()}
    , m_markLegacyArrays{m_options[s_mark_legacy_arrays].toBoolean()}
{}

VariableUnserializer::Type VariableUnserializer::type() const {
  return m_type;
}

bool VariableUnserializer::allowUnknownSerializableClass() const {
  return m_unknownSerializable;
}

const char* VariableUnserializer::head() const {
  return m_buf;
}

const char* VariableUnserializer::begin() const {
  return m_begin;
}

const char* VariableUnserializer::end() const {
  return m_end;
}

char VariableUnserializer::peek() const {
  check();
  return *m_buf;
}

char VariableUnserializer::peekBack() const {
  return m_buf[-1];
}

bool VariableUnserializer::endOfBuffer() const {
  return m_buf >= m_end;
}

char VariableUnserializer::readChar() {
  check();
  return *(m_buf++);
}

void VariableUnserializer::add(tv_lval v, UnserializeMode mode) {
  switch (mode) {
    case UnserializeMode::Value:  m_refs.emplace_back(v); break;
    // We don't support refs to collection keys; use nullptr as a sentinel.
    case UnserializeMode::ColKey: m_refs.emplace_back(nullptr); break;
    case UnserializeMode::Key:    break;
  }
}

void VariableUnserializer::reserveForAdd(size_t count) {
  // If the array is large, the space for the backrefs could be
  // significant, so we need to check for OOM beforehand. To do this,
  // we need to do some guess work to estimate what memory the vector
  // will consume once we've done the reserve (we assume the vector
  // doubles in capacity as necessary).
  auto const newSize = m_refs.size() + count;
  auto const capacity = m_refs.capacity();
  if (newSize <= capacity) return;
  auto const total =
    (folly::nextPowTwo(newSize) - capacity) *
    sizeof(decltype(m_refs)::value_type);
  if (UNLIKELY(total > kMaxSmallSize && tl_heap->preAllocOOM(total))) {
    check_non_safepoint_surprise();
  }
  m_refs.reserve(newSize);
}

TypedValue VariableUnserializer::getByVal(int id) {
  if (id <= 0 || id > m_refs.size()) throwOutOfRange(id);
  auto const result = m_refs[id - 1];
  if (!result) throwColRKey();
  return result.tv();
}

void VariableUnserializer::check() const {
  if (m_buf >= m_end) throwUnexpectedEOB();
}

void VariableUnserializer::checkElemTermination() const {
  auto const ch = peekBack();
  if (ch != ';' && ch != '}') throwUnterminatedElement();
}

void VariableUnserializer::set(const char* buf, const char* end) {
  m_buf = buf;
  m_end = end;
}

Variant VariableUnserializer::unserialize() {
  Variant v;
  unserializeVariant(v.asTypedValue());
  if (UNLIKELY(StructuredLog::coinflip(RuntimeOption::EvalSerDesSampleRate))) {
    String ser(m_begin, m_end - m_begin, CopyString);
    auto const fmt = folly::sformat("VU{}", (int)m_type);
    StructuredLog::logSerDes(fmt.c_str(), "des", ser, v);
  }

  for (auto& obj : m_sleepingObjects) {
    obj->invokeWakeup();
  }

  return v;
}

namespace {
std::pair<int64_t,const char*> hh_strtoll_base10(const char* p) {
  int64_t x = 0;
  bool neg = false;
  if (*p == '-') {
    neg = true;
    ++p;
  }
  while (*p >= '0' && *p <= '9') {
    x = (x * 10) + ('0' - *p);
    ++p;
  }
  if (!neg) {
    x = -x;
  }
  return std::pair<int64_t,const char*>(x, p);
}
}

int64_t VariableUnserializer::readInt() {
  check();
  auto r = hh_strtoll_base10(m_buf);
  m_buf = r.second;
  return r.first;
}

double VariableUnserializer::readDouble() {
  check();
  const char* newBuf;
  double r = zend_strtod(m_buf, &newBuf);
  m_buf = newBuf;
  return r;
}

folly::StringPiece VariableUnserializer::readStr(unsigned n) {
  check();
  auto const bufferLimit = std::min(size_t(m_end - m_buf), size_t(n));
  auto str = folly::StringPiece(m_buf, bufferLimit);
  m_buf += bufferLimit;
  return str;
}

void VariableUnserializer::expectChar(char expected) {
  char ch = readChar();
  if (UNLIKELY(ch != expected)) {
    throwUnexpectedSep(expected, ch);
  }
}

namespace {
bool isWhitelistClass(const String& requestedClassName,
                      const Array& list,
                      bool includeSubclasses) {
  if (!list.empty()) {
    for (ArrayIter iter(list); iter; ++iter) {
      auto allowedClassName = iter.second().toString();
      auto const matches = includeSubclasses
        ? HHVM_FN(is_a)(requestedClassName, allowedClassName, true)
        : allowedClassName.get()->isame(requestedClassName.get());
      if (matches) return true;
    }
  }
  return false;
}
}

const StaticString s_throw("throw");
const StaticString s_allowed_classes("allowed_classes");
const StaticString s_include_subclasses("include_subclasses");

bool VariableUnserializer::whitelistCheck(const String& clsName) const {
  if (m_type != Type::Serialize || m_options.isNull()) {
    return true;
  }

  // PHP7-style class whitelisting
  // Allowed classes are allowed,
  // all others result in __Incomplete_PHP_Class
  if (m_options.exists(s_allowed_classes)) {
    auto allowed_classes = m_options[s_allowed_classes];
    auto const ok = [&] {
      if (allowed_classes.isArray()) {
        auto const subs = m_options[s_include_subclasses].toBoolean();
        return isWhitelistClass(clsName,
                                allowed_classes.toArray(),
                                subs);
      } else if (allowed_classes.isBoolean()) {
        return allowed_classes.toBoolean();
      } else {
        throw InvalidAllowedClassesException();
      }
    }();

    if (!ok && m_options[s_throw].toBoolean()) {
      throw_object(m_options[s_throw].toString(),
                   make_vec_array(clsName));
    }
    return ok;
  }

  if (!RuntimeOption::UnserializationWhitelistCheck) {
    // No need for BC HHVM-style whitelist check,
    // since the check isn't enabled.
    // Go with PHP5 default behavior of allowing all
    return true;
  }

  // Check for old-style whitelist
  if (isWhitelistClass(clsName, m_options, false)) {
    return true;
  }

  // Non-whitelisted class with a check enabled,
  // are we willing to hard-error over it?
  const char* err_msg =
    "The object being unserialized with class name '%s' "
    "is not in the given whitelist"; // followed by ' in <filename> on line %d'.

  if (RuntimeOption::UnserializationWhitelistCheckWarningOnly) {
    // Nope, just whine to the user and let it through
    raise_warning(err_msg, clsName.c_str());
    return true;
  } else {
    // Yes, shut it down.
    raise_error(err_msg, clsName.c_str());
    return false;
  }
}

void VariableUnserializer::addSleepingObject(const Object& o) {
  m_sleepingObjects.emplace_back(o);
}

bool VariableUnserializer::matchString(folly::StringPiece str) {
  const char* p = m_buf;
  assertx(p <= m_end);
  int total = 0;
  if (*p == 'S' && type() == VariableUnserializer::Type::APCSerialize) {
    total = 2 + 8 + 1;
    if (p + total > m_end) return false;
    p++;
    if (*p++ != ':') return false;
    auto const sd = *reinterpret_cast<StringData*const*>(p);
    assertx(sd->isStatic());
    if (str.compare(sd->slice()) != 0) return false;
    p += size_t(8);
  } else {
    const auto ss = str.size();
    if (ss >= 100) return false;
    int digits = ss >= 10 ? 2 : 1;
    total = 2 + digits + 2 + ss + 2;
    if (p + total > m_end) return false;
    if (*p++ != 's') return false;
    if (*p++ != ':') return false;
    if (digits == 2) {
      if (*p++ != '0' + ss/10) return false;
      if (*p++ != '0' + ss%10) return false;
    } else {
      if (*p++ != '0' + ss) return false;
    }
    if (*p++ != ':') return false;
    if (*p++ != '\"') return false;
    if (memcmp(p, str.data(), ss)) return false;
    p += ss;
    if (*p++ != '\"') return false;
  }
  if (*p++ != ';') return false;
  assertx(m_buf + total == p);
  m_buf = p;
  return true;
}

///////////////////////////////////////////////////////////////////////////////

// remainingProps should include the current property being unserialized.
void VariableUnserializer::unserializePropertyValue(tv_lval v,
                                                    int remainingProps) {
  assertx(remainingProps > 0);
  unserializeVariant(v);
  if (--remainingProps > 0) {
    auto lastChar = peekBack();
    if (lastChar != ';' && lastChar != '}') {
      throwUnterminatedProperty();
    }
  }
}

// nProp should include the current property being unserialized.
NEVER_INLINE
void VariableUnserializer::unserializeProp(ObjectData* obj,
                                           const String& key,
                                           Class* ctx,
                                           const String& realKey,
                                           int nProp) {

  auto const cls = obj->getVMClass();
  auto const lookup = cls->getDeclPropSlot(ctx, key.get());
  auto const slot = lookup.slot;
  tv_lval t;

  if (slot == kInvalidSlot || !lookup.accessible) {
    // Unserialize as a dynamic property. If this is the first, we need to
    // pre-allocate space in the array to ensure the elements don't move during
    // unserialization.
    obj->reserveDynProps(nProp);
    t = obj->makeDynProp(realKey.get());
  } else {
    // We'll check if this doesn't violate the type-hint once we're done
    // unserializing all the props.
    t = obj->getPropLval(ctx, key.get());
  }

  unserializePropertyValue(t, nProp);
  if (!RuntimeOption::RepoAuthoritative) return;
  if (!Repo::get().global().HardPrivatePropInference) return;

  /*
   * We assume for performance reasons in repo authoriative mode that
   * we can see all the sets to private properties in a class.
   *
   * It's a hole in this if we don't check unserialization doesn't
   * violate what we've seen, which we handle by throwing if the repo
   * was built with this option.
   */
  if (UNLIKELY(slot == kInvalidSlot)) return;
  auto const repoTy = cls->declPropRepoAuthType(slot);
  if (LIKELY(tvMatchesRepoAuthType(*t, repoTy))) return;
  if (t.type() == KindOfUninit &&
      (cls->declProperties()[slot].attrs & AttrLateInit)) {
    return;
  }
  throwUnexpectedType(key, obj, *t);
}


NEVER_INLINE
void VariableUnserializer::unserializeRemainingProps(
  Object& obj,
  int remainingProps,
  Variant& serializedNativeData,
  bool& hasSerializedNativeData) {
  obj->unlockObject();
  SCOPE_EXIT { obj->lockObject(); };
  while (remainingProps > 0) {
    /*
      use the number of properties remaining as an estimate for
      the total number of dynamic properties when we see the
      first dynamic prop.  see getVariantPtr
    */
    Variant v;
    unserializeVariant(v.asTypedValue(), UnserializeMode::Key);
    String key = v.toString();
    int ksize = key.size();
    const char *kdata = key.data();
    int subLen = 0;
    if (key == s_serializedNativeDataKey) {
      unserializePropertyValue(serializedNativeData.asTypedValue(),
                               remainingProps--);
      hasSerializedNativeData = true;
    } else if (kdata[0] == '\0') {
      if (UNLIKELY(!ksize)) {
        raise_error("Cannot access empty property");
      }
      // private or protected
      subLen = strlen(folly::launder(kdata) + 1) + 2;
      if (UNLIKELY(subLen >= ksize)) {
        if (subLen == ksize) {
          raise_error("Cannot access empty property");
        } else {
          throwMangledPrivateProperty();
        }
      }
      String k(kdata + subLen, ksize - subLen, CopyString);
      Class* ctx = (Class*)-1;
      if (kdata[1] != '*') {
        ctx = Unit::lookupClass(
          String(kdata + 1, subLen - 2, CopyString).get());
      }
      unserializeProp(obj.get(), k, ctx, key,
                      remainingProps--);
    } else {
      unserializeProp(obj.get(), key, nullptr, key,
                      remainingProps--);
    }
  }
}

namespace {

static const StaticString
  s_Vector("Vector"), s_HH_Vector("HH\\Vector"),
  s_Map("Map"), s_HH_Map("HH\\Map"),
  s_Set("Set"), s_HH_Set("HH\\Set"),
  s_Pair("Pair"), s_HH_Pair("HH\\Pair"),
  s_StableMap("StableMap");

/*
 * For namespaced collections, returns an "alternate" name, which is a
 * collection name with or without the namespace qualifier, depending on
 * what's passed.
 * If no alternate name is found, returns nullptr.
 */
const StringData* getAlternateCollectionName(const StringData* clsName) {
  typedef hphp_hash_map<const StringData*, const StringData*,
                        string_data_hash, string_data_isame> ClsNameMap;

  auto getAltMap = [] {
    typedef std::pair<StaticString, StaticString> SStringPair;

    static ClsNameMap m;

    static std::vector<SStringPair> mappings {
      std::make_pair(s_Vector, s_HH_Vector),
      std::make_pair(s_Map, s_HH_Map),
      std::make_pair(s_Set, s_HH_Set),
      std::make_pair(s_Pair, s_HH_Pair)
    };

    for (const auto& p : mappings) {
      m[p.first.get()] = p.second.get();
      m[p.second.get()] = p.first.get();
    }

    // As part of StableMap merging into Map, StableMap is an alias for HH\\Map,
    // but Map is the sole alias for HH\\Map
    m[s_StableMap.get()] = s_HH_Map.get();
    return &m;
  };

  static const ClsNameMap* altMap = getAltMap();

  auto it = altMap->find(clsName);
  return it != altMap->end() ? it->second : nullptr;
}

Class* tryAlternateCollectionClass(const StringData* clsName) {
  auto altName = getAlternateCollectionName(clsName);
  return altName ? Unit::getClass(altName, /* autoload */ false) : nullptr;
}

/*
 * Try to read 'str' while advancing 'cur' without reaching 'end'.
 */
ALWAYS_INLINE
static bool match(const char*& cur,
                  const char* expected,
                  const char* const end) {
  if (cur + strlen(expected) >= end) return false;
  while (*expected) {
    if (*cur++ != *expected++) return false;
  }
  return true;
}

ALWAYS_INLINE
static int64_t read64(const char*& cur) {
  auto p = hh_strtoll_base10(cur);
  cur = p.second;
  return p.first;
}

/*
 * Read an int64 from 'cur' into 'out'. Returns false on unexpected
 * (but possibly still legal) format or if 'end' is reached.
 */
ALWAYS_INLINE
bool readInt64(const char*& cur, const char* const end, int64_t& out) {
  if (!match(cur, "i:", end)) return false;
  out = read64(cur);
  return match(cur, ";", end);
}

/*
 * Read, allocate, and return a string from 'cur'. Returns null on unexpected
 * (but possibly still legal) format or if 'end' is reached, without allocating.
 */
ALWAYS_INLINE
static StringData* readStringData(const char*& cur, const char* const end,
                                  int maxLen) {
  if (!match(cur, "s:", end)) return nullptr;
  auto len = read64(cur);
  if (len < 0 || len >= maxLen) return nullptr;
  if (!match(cur, ":\"", end)) return nullptr;
  auto const slice = folly::StringPiece(cur, len);
  if ((cur += len) >= end) return nullptr;
  if (!match(cur, "\";", end)) return nullptr;
  // TODO(11398853): Consider streaming/non-temporal stores here.
  auto sd = StringData::Make(slice, CopyString);
  return sd;
}
}

NEVER_INLINE
void VariableUnserializer::unserializeVariant(
    tv_lval self,
    UnserializeMode mode /* = UnserializeMode::Value */) {

  // If we're overwriting an array element or property value, save the old
  // value in case it's later referenced via an r: or R: ref.
  if (isRefcountedType(self.type()) && mode == UnserializeMode::Value) {
    m_overwrittenList.append(*self);
  }

  // NOTE: If you make changes to how serialization and unserialization work,
  // make sure to update reserialize() here and test_apc_reserialize()
  // in "test/ext/test_ext_apc.cpp".

  char type = readChar();
  char sep = readChar();

  if (type != 'R') {
    add(self, mode);
  }

  if (type == 'N') {
    if (sep != ';') throwUnexpectedSep(';', sep);
    tvSetNull(self); // NULL *IS* the value, without we get undefined warnings
    return;
  }
  if (sep != ':') throwUnexpectedSep(':', sep);

  switch (type) {
  case 'r':
  case 'R':
    {
      int64_t id = readInt();
      tvSet(getByVal(id), self);
    }
    break;
  case 'b':
    {
      int64_t v = readInt();
      tvSetBool((bool)v, self);
      break;
    }
  case 'i':
    {
      int64_t v = readInt();
      tvSetInt(v, self);
      break;
    }
  case 'd':
    {
      char ch = peek();
      bool negative = false;
      if (ch == '-') {
        negative = true;
        readChar();
        ch = peek();
      }
      double v;
      if (ch == 'I') {
        auto str = readStr(3);
        if (str.size() != 3 || str[1] != 'N' || str[2] != 'F') {
          throwUnexpectedStr("INF", str);
        }
        v = std::numeric_limits<double>::infinity();
      } else if (ch == 'N') {
        auto str = readStr(3);
        if (str.size() != 3 || str[1] != 'A' || str[2] != 'N') {
          throwUnexpectedStr("NAN", str);
        }
        v = std::numeric_limits<double>::quiet_NaN();
      } else {
        v = readDouble();
      }
      tvSetDouble(negative ? -v : v, self);
    }
    break;
  case 's':
    {
      String v = unserializeString();
      tvMove(make_tv<KindOfString>(v.detach()), self);
      if (!endOfBuffer()) {
        // Semicolon *should* always be required,
        // but PHP's implementation allows omitting it
        // and still functioning.
        // Worse, it throws it away without any check.
        // So we'll do the same.  Sigh.
        readChar();
      }
    }
    return;
  case 'S':
    if (this->type() == VariableUnserializer::Type::APCSerialize) {
      auto str = readStr(8);
      assertx(str.size() == 8);
      auto const sd = *reinterpret_cast<StringData*const*>(&str[0]);
      assertx(sd->isStatic());
      tvMove(make_tv<KindOfPersistentString>(sd), self);
    } else {
      throwUnknownType(type);
    }
    break;
  case 'a': // PHP array
  case 'D': // Dict
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      // It seems silly to check this here, but GCC actually generates much
      // better code this way.
      auto a = (type == 'a') ?
        unserializeArray() :
        unserializeDict();
      if (UNLIKELY(m_markLegacyArrays && type == 'a')) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'x': // legacy dict
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeDict();
      a.setLegacyArray(true);
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'Y': // DArray
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeDArray();
      if (UNLIKELY(m_markLegacyArrays)) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'y': // VArray
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeVArray();
      if (UNLIKELY(m_markLegacyArrays)) {
        a.setLegacyArray(true);
      }
      tvMove(make_array_like_tv(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'v': // Vec
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeVec();
      tvMove(make_tv<KindOfVec>(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'X': // legacy vec
  {
    // Check stack depth to avoid overflow.
    check_recursion_throw();
    auto a = unserializeVec();
    a.setLegacyArray(true);
    tvMove(make_tv<KindOfVec>(a.detach()), self);
  }
  return; // array has '}' terminating
  case 'k': // Keyset
    {
      // Check stack depth to avoid overflow.
      check_recursion_throw();
      auto a = unserializeKeyset();
      tvMove(make_tv<KindOfKeyset>(a.detach()), self);
    }
    return; // array has '}' terminating
  case 'L':
    {
      int64_t id = readInt();
      expectChar(':');
      String rsrcName = unserializeString();
      expectChar('{');
      expectChar('}');
      auto rsrc = req::make<DummyResource>();
      rsrc->o_setResourceId(id);
      rsrc->m_class_name = std::move(rsrcName);
      tvMove(make_tv<KindOfResource>(rsrc.detach()->hdr()), self);
    }
    return; // resource has '}' terminating
  case 'O':
  case 'V':
  case 'K':
    {
      String clsName = unserializeString();

      expectChar(':');
      const int64_t size = readInt();
      expectChar(':');
      expectChar('{');

      const bool allowObjectFormatForCollections = true;

      Class* cls = nullptr;

      // If we are potentially dealing with a collection, we need to try to
      // load the collection class under an alternate name so that we can
      // deserialize data that was serialized before the migration of
      // collections to the HH namespace.

      if (type == 'O') {
        if (whitelistCheck(clsName)) {
          if (allowObjectFormatForCollections) {
            // In order to support the legacy {O|V}:{Set|Vector|Map}
            // serialization, we defer autoloading until we know that there's
            // no alternate (builtin) collection class.
            cls = Unit::getClass(clsName.get(), /* autoload */ false);
            if (!cls) {
              cls = tryAlternateCollectionClass(clsName.get());
            }
          }

          // No valid class was found, lets try the autoloader.
          if (!cls) {
            if (!is_valid_class_name(clsName.slice())) {
              throwInvalidClassName();
            }
            cls = Unit::loadClass(clsName.get()); // with autoloading
          }
        }
      } else {
        // Collections are CPP builtins; don't attempt to autoload
        cls = Unit::getClass(clsName.get(), /* autoload */ false);
        if (!cls) {
          cls = tryAlternateCollectionClass(clsName.get());
        }
        if (!cls || !cls->isCollectionClass()) {
          throwNotCollection(clsName);
        }
      }

      Object obj;
      auto remainingProps = size;
      if (cls) {
        // Only unserialize CPP extension types which can actually support
        // it. Otherwise, we risk creating a CPP object without having it
        // initialized completely.
        if (cls->instanceCtor() && !cls->isCppSerializable() &&
            !cls->isCollectionClass()) {
          assertx(obj.isNull());
          throw_null_pointer_exception();
        } else {
          if (UNLIKELY(collections::isType(cls, CollectionType::Pair))) {
            if (UNLIKELY(size != 2)) {
              throwInvalidPair();
            }
            // pairs can't be constructed without elements
            obj = Object{req::make<c_Pair>(make_tv<KindOfNull>(),
                                           make_tv<KindOfNull>(),
                                           c_Pair::NoIncRef{})};
          } else if (UNLIKELY(cls->hasReifiedGenerics())) {
            // First prop on the serialized list is the reified generics prop
            if (!matchString(s_86reified_prop.slice())) {
              throwInvalidOFormat(clsName);
            }
            TypedValue tv = make_tv<KindOfNull>();
            auto const t = tv_lval{&tv};
            unserializePropertyValue(t, remainingProps--);
            if (!TypeStructure::coerceToTypeStructureList_SERDE_ONLY(t)) {
              throwInvalidOFormat(clsName);
            }
            assertx(tvIsHAMSafeVArray(t));
            obj = Object{cls, t.val().parr};
          } else {
            obj = Object{cls};
          }
        }
      } else {
        warnOrThrowUnknownClass(clsName);
        obj = Object{SystemLib::s___PHP_Incomplete_ClassClass};
        obj->setProp(nullptr, s_PHP_Incomplete_Class_Name.get(),
                     clsName.asTypedValue());
      }
      assertx(!obj.isNull());
      tvSet(make_tv<KindOfObject>(obj.get()), self);

      if (remainingProps > 0) {
        // Check stack depth to avoid overflow.
        check_recursion_throw();

        if (type == 'O') {
          // Collections are not allowed
          if (obj->isCollection()) {
            throwInvalidOFormat(clsName);
          }

          Variant serializedNativeData = init_null();
          bool hasSerializedNativeData = false;
          bool checkRepoAuthType =
            RuntimeOption::RepoAuthoritative &&
            Repo::get().global().HardPrivatePropInference;
          Class* objCls = obj->getVMClass();
          // Try fast case.
          if (remainingProps >= objCls->numDeclProperties() -
                                (objCls->hasReifiedGenerics() ? 1 : 0)) {
            auto mismatch = false;
            auto const objProps = obj->props();

            auto const declProps = objCls->declProperties();
            for (auto const& p : declProps) {
              auto slot = p.serializationIdx;
              auto index = objCls->propSlotToIndex(slot);
              auto const& prop = declProps[slot];
              if (prop.name == s_86reified_prop.get()) continue;
              if (!matchString(prop.mangledName->slice())) {
                mismatch = true;
                break;
              }

              // don't need to worry about overwritten list, because
              // this is definitely the first time we're setting this
              // property.
              auto const t = objProps->at(index);
              unserializePropertyValue(t, remainingProps--);

              if (UNLIKELY(checkRepoAuthType &&
                           !tvMatchesRepoAuthType(*t, prop.repoAuthType))) {
                throwUnexpectedType(prop.name, obj.get(), *t);
              }
            }
            // If everything matched, all remaining properties are dynamic.
            if (!mismatch && remainingProps > 0) {
              // the dynPropTable can be mutated while we're deserializing
              // the contents of this object's prop array. Don't hold a
              // reference to this object's entry in the table while looping.
              obj->reserveDynProps(remainingProps);
              while (remainingProps > 0) {
                Variant v;
                unserializeVariant(v.asTypedValue(), UnserializeMode::Key);
                String key = v.toString();
                if (key == s_serializedNativeDataKey) {
                  unserializePropertyValue(serializedNativeData.asTypedValue(),
                                           remainingProps--);
                  hasSerializedNativeData = true;
                } else {
                  auto kdata = key.data();
                  if (kdata[0] == '\0') {
                    auto ksize = key.size();
                    if (UNLIKELY(ksize == 0)) {
                      raise_error("Cannot access empty property");
                    }
                    // private or protected
                    auto subLen = strlen(folly::launder(kdata) + 1) + 2;
                    if (UNLIKELY(subLen >= ksize)) {
                      if (subLen == ksize) {
                        raise_error("Cannot access empty property");
                      } else {
                        throwMangledPrivateProperty();
                      }
                    }
                  }
                  auto const lval = obj->makeDynProp(key.get());
                  unserializePropertyValue(lval, remainingProps--);
                }
              }
            }
          }
          if (remainingProps > 0) {
            INC_TPC(unser_prop_slow);
            unserializeRemainingProps(obj, remainingProps,
                                      serializedNativeData,
                                      hasSerializedNativeData);
            remainingProps = 0;
          } else {
            INC_TPC(unser_prop_fast);
          }

          // Verify that all the unserialized properties satisfy their
          // type-hints. Its safe to do it like this (after we've set the values
          // in the properties) because this object hasn't escaped to the
          // outside world yet.
          obj->verifyPropTypeHints();

          // nativeDataWakeup is called last to ensure that all properties are
          // already unserialized. We also ensure that nativeDataWakeup is
          // invoked regardless of whether or not serialized native data exists
          // within the serialized content.
          if (obj->hasNativeData() &&
              obj->getVMClass()->getNativeDataInfo()->isSerializable()) {
            Native::nativeDataWakeup(obj.get(), serializedNativeData);
          } else if (hasSerializedNativeData) {
            raise_warning("%s does not expect any serialized native data.",
                          clsName.data());
          }
        } else {
          assertx(type == 'V' || type == 'K');
          if (!obj->isCollection()) {
            throwNotCollection(clsName);
          }
          unserializeCollection(obj.get(), size, type);
        }
      }
      expectChar('}');

      if (cls &&
          cls->lookupMethod(s___wakeup.get()) &&
          (this->type() != VariableUnserializer::Type::DebuggerSerialize ||
           (cls->instanceCtor() && cls->isCppSerializable()))) {
        // Don't call wakeup when unserializing for the debugger, except for
        // natively implemented classes.
        addSleepingObject(obj);
      }

      check_non_safepoint_surprise();
    }
    return; // object has '}' terminating
  case 'C':
    {
      if (this->type() == VariableUnserializer::Type::DebuggerSerialize) {
        raise_error("Debugger shouldn't call custom unserialize method");
      }
      String clsName = unserializeString();

      expectChar(':');
      String serialized = unserializeString('{', '}');

      auto obj = [&]() -> Object {
        if (whitelistCheck(clsName)) {
          // Try loading without the autoloader first
          auto cls = Unit::getClass(clsName.get(), /* autoload */ false);
          if (!cls) {
            if (!is_valid_class_name(clsName.slice())) {
              throwInvalidClassName();
            }
            cls = Unit::loadClass(clsName.get());
          }
          if (cls) {
            return Object::attach(g_context->createObject(cls, init_null_variant,
                                                          false /* init */));
          }
        }
        if (!allowUnknownSerializableClass()) {
          raise_error("unknown class %s", clsName.data());
        }
        warnOrThrowUnknownClass(clsName);
        Object ret = create_object_only(s_PHP_Incomplete_Class);
        ret->setProp(nullptr, s_PHP_Incomplete_Class_Name.get(),
                     clsName.asTypedValue());
        ret->setProp(nullptr, s_serialized.get(), serialized.asTypedValue());
        return ret;
      }();

      if (!obj->instanceof(SystemLib::s_SerializableClass)) {
        raise_warning("Class %s has no unserializer",
                      obj->getClassName().data());
      } else {
        obj->o_invoke_few_args(s_unserialize, 1, serialized);
      }

      tvMove(make_tv<KindOfObject>(obj.detach()), self);
    }
    return; // object has '}' terminating
  default:
    throwUnknownType(type);
  }
  expectChar(';');
}

Array VariableUnserializer::unserializeArray() {
  int64_t size = readInt();
  expectChar(':');
  expectChar('{');
  if (size == 0) {
    expectChar('}');
    return m_forceDArrays || type() == Type::Serialize
      ? Array::CreateDArray()
      : Array::Create();
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }
  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  // Pre-allocate an ArrayData of the given size, to avoid escalation in the
  // middle, which breaks references.
  auto arr = m_forceDArrays || type() == Type::Serialize
    ? DArrayInit(size).toArray()
    : MixedArrayInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  return arr;
}

arrprov::Tag VariableUnserializer::unserializeProvenanceTag() {
  auto const finish = [&] (auto tag) -> arrprov::Tag {
    if (!RuntimeOption::EvalArrayProvenance) return {};
    return tag;
  };

  auto const read_filename = [&]() -> const StringData* {
    if (peek() == 't') {
      assertx(m_unitFilename);
      expectChar('t');
      return m_unitFilename;
    } else {
      expectChar('s');
      expectChar(':');
      return makeStaticString(unserializeString().get());
    }
  };

  if (type() != VariableUnserializer::Type::Internal) return {};
  if (peek() == 'p') {
    expectChar('p');
    expectChar(':');
    expectChar('i');
    expectChar(':');
    auto const line = static_cast<int>(readInt());
    expectChar(';');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag { filename, line }
    );
  } else if (peek() == 'P') {
    expectChar('P');
    expectChar(';');
    return finish(
      arrprov::Tag::RepoUnion()
    );
  } else if (peek() == 'r') {
    expectChar('r');
    expectChar(':');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag::TraitMerge(filename)
    );
  } else if (peek() == 'e') {
    expectChar('e');
    expectChar(':');
    auto const filename = read_filename();
    expectChar(';');
    return finish(
      arrprov::Tag::LargeEnum(filename)
    );
  } else {
    return {};
  }
}

Array VariableUnserializer::unserializeDict() {
  if (m_dvOverrides) m_dvOverrides->push_back(false);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvHackArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyDictArray(), provTag)
      : staticEmptyDictArray()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  Array arr = DictInit(size).toArray();
  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeVec() {
  if (m_dvOverrides) m_dvOverrides->push_back(false);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvHackArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyVec(), provTag)
      : staticEmptyVec()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }
  auto const sizeClass = PackedArray::capacityToSizeIndex(size);
  auto const allocsz = MemoryManager::sizeIndex2Size(sizeClass);

  // For large arrays, do a naive pre-check for OOM.
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  Array arr = VecInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    unserializeVariant(PackedArray::LvalNewInPlace(arr.get()));
    if (i < size - 1) checkElemTermination();
  }
  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeVArray() {
  if (m_dvOverrides) m_dvOverrides->push_back(true);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvDVArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    if (m_type != Type::Serialize) {
      return Array::attach(provTag
        ? arrprov::tagStaticArr(staticEmptyVArray(), provTag)
        : staticEmptyVArray()
      );
    }
    return m_forceDArrays
      ? Array::attach(provTag
          ? arrprov::tagStaticArr(staticEmptyDArray(), provTag)
          : staticEmptyDArray()
        )
      : Array::attach(provTag
          ? arrprov::tagStaticArr(staticEmptyVArray(), provTag)
          : staticEmptyVArray()
        );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  auto const oomCheck = [&](size_t allocsz) {
    // For large arrays, do a naive pre-check for OOM.
    if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
      check_non_safepoint_surprise();
    }
  };

  auto arr = Array{};
  if (m_forceDArrays && m_type == Type::Serialize) {
    // Deserialize to vector-ish darray. Use direct calls to MixedArray.
    oomCheck(MixedArray::computeAllocBytesFromMaxElms(size));

    arr = DArrayInit(size).toArray();
    reserveForAdd(size);

    for (int64_t i = 0; i < size; i++) {
      unserializeVariant(MixedArray::LvalInPlace(arr.get(), i));
      if (i < size - 1) checkElemTermination();
    }
  } else {
    // Deserialize to varray. Use direct calls to MixedArray.
    auto const index = PackedArray::capacityToSizeIndex(size);
    oomCheck(MemoryManager::sizeIndex2Size(index));

    arr = VArrayInit(size).toArray();
    reserveForAdd(size);

    for (int64_t i = 0; i < size; i++) {
      unserializeVariant(PackedArray::LvalNewInPlace(arr.get()));
      if (i < size - 1) checkElemTermination();
    }
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeDArray() {
  if (m_dvOverrides) m_dvOverrides->push_back(true);

  int64_t size = readInt();
  expectChar(':');
  expectChar('{');

  auto provTag = unserializeProvenanceTag();
  if (!RO::EvalArrProvDVArrays) provTag = {};

  if (size == 0) {
    expectChar('}');
    return Array::attach(provTag
      ? arrprov::tagStaticArr(staticEmptyDArray(), provTag)
      : staticEmptyDArray()
    );
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto arr = DArrayInit(size).toArray();
  reserveForAdd(size);

  for (int64_t i = 0; i < size; i++) {
    Variant key;
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);
    if (!key.isString() && !key.isInteger()) throwInvalidKey();
    unserializeVariant(MixedArray::LvalInPlace(arr.get(), key));
    if (i < size - 1) checkElemTermination();
  }

  check_non_safepoint_surprise();
  expectChar('}');
  if (provTag) arrprov::setTag<arrprov::Mode::Emplace>(arr.get(), provTag);
  return arr;
}

Array VariableUnserializer::unserializeKeyset() {
  int64_t size = readInt();
  expectChar(':');
  expectChar('{');
  if (size == 0) {
    expectChar('}');
    return Array::CreateKeyset();
  }
  if (UNLIKELY(size < 0 || size > std::numeric_limits<int>::max())) {
    throwArraySizeOutOfBounds();
  }

  // For large arrays, do a naive pre-check for OOM.
  auto const allocsz = SetArray::computeAllocBytesFromMaxElms(size);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  KeysetInit init(size);
  for (int64_t i = 0; i < size; i++) {
    Variant key;
    // Use key mode to stop the unserializer from keeping a pointer to this
    // variant (since its stack-allocated).
    unserializeVariant(key.asTypedValue(), UnserializeMode::Key);

    auto const type = key.getType();
    if (UNLIKELY(!isStringType(type) && !isIntType(type))) {
      throwKeysetValue();
    }

    init.add(key);

    if (i < (size - 1)) {
      auto lastChar = peekBack();
      if ((lastChar != ';' && lastChar != '}')) {
        throwUnterminatedElement();
      }
    }
  }
  check_non_safepoint_surprise();
  expectChar('}');
  return init.toArray();
}


folly::StringPiece
VariableUnserializer::unserializeStringPiece(char delimiter0, char delimiter1) {
  int64_t size = readInt();
  if (size >= RuntimeOption::MaxSerializedStringSize) {
    throwLargeStringSize(size);
  }
  if (size < 0) {
    throwNegativeStringSize(size);
  }
  expectChar(':');
  expectChar(delimiter0);
  auto const piece = readStr(size);
  expectChar(delimiter1);
  return piece;
}

String VariableUnserializer::unserializeString(char delimiter0,
                                               char delimiter1) {
  auto const piece = unserializeStringPiece(delimiter0, delimiter1);
  return String::attach(readOnly() ?
                        makeStaticString(piece) :
                        StringData::Make(piece, CopyString));
}

void VariableUnserializer::unserializeCollection(ObjectData* obj, int64_t sz,
                                                 char type) {
  switch (obj->collectionType()) {
    case CollectionType::Pair:
      unserializePair(obj, sz, type);
      break;
    case CollectionType::Vector:
    case CollectionType::ImmVector:
      unserializeVector(obj, sz, type);
      break;
    case CollectionType::Map:
    case CollectionType::ImmMap:
      unserializeMap(obj, sz, type);
      break;
    case CollectionType::Set:
    case CollectionType::ImmSet:
      unserializeSet(obj, sz, type);
      break;
  }
}

void VariableUnserializer::unserializeVector(ObjectData* obj, int64_t sz,
                                             char type) {
  if (type != 'V') throwBadFormat(obj, type);

  auto const sizeClass = PackedArray::capacityToSizeIndex(sz);
  auto const allocsz = MemoryManager::sizeIndex2Size(sizeClass);
  // For large vectors, do a naive pre-check for OOM.
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto bvec = static_cast<BaseVector*>(obj);
  bvec->reserve(sz);
  reserveForAdd(sz);
  for (int64_t i = 0; i < sz; ++i) {
    auto tv = bvec->appendForUnserialize(i);
    HPHP::type(tv) = KindOfNull;
    unserializeVariant(tv);
  }
}

/*
 * Attempts to unserialize into an initially empty HH\Map of string->int/string.
 * Returns false and leaves both 'map' and 'uns' untouched on failure, including
 * unexpected types and possibly legal, but uncommon, encodings.
 */
NEVER_INLINE
bool VariableUnserializer::tryUnserializeStrIntMap(BaseMap* map, int64_t sz) {
  auto b = head();
  /*
   * For efficiency, we don't add the keys/values to m_refs, so don't support
   * back-references appearing after this point. For simplicity, we thus require
   * this map to be the root object being unserialized.
   */
  if (folly::StringPiece(begin(), b) !=
      folly::to<std::string>("K:6:\"HH\\Map\":", sz, ":{")) {
    return false;
  }
  auto const end = this->end();
  auto const maxKeyLen = RuntimeOption::MaxSerializedStringSize;
  /*
   * First, parse the entire input and allocate the keys (accessing lots of
   * data, but mostly sequentially).
   */
  auto checkPoint = map->batchInsertBegin(sz);
  int64_t i = 0;
  for (; i < sz; ++i) {
    auto sd = readStringData(b, end, maxKeyLen);
    if (!sd) break;
    String key = String::attach(sd);
    auto tv = map->batchInsert(key.get());
    tv->m_type = KindOfNull;
    if (*b == 'i') {
      if (!readInt64(b, end, tv->m_data.num)) break;
      tv->m_type = KindOfInt64;
    } else if (*b == 's') {
      auto sd = readStringData(b, end, maxKeyLen);
      if (!sd) break;
      tv->m_data.pstr = sd;
      tv->m_type = KindOfString;
    } else {
      break;
    }
  }
  /*
   * On success, finalize the hash table insertion (very random access).
   */
  if (i == sz && map->tryBatchInsertEnd(checkPoint)) {
    set(b, end);
    return true;
  }
  map->batchInsertAbort(checkPoint);
  return false;
}

void VariableUnserializer::unserializeMap(ObjectData* obj, int64_t sz,
                                          char type) {
  if (type != 'K') throwBadFormat(obj, type);

  // For large maps, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(sz);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto map = static_cast<BaseMap*>(obj);
  map->reserve(sz);
  if (sz >= RuntimeOption::UnserializationBigMapThreshold &&
      tryUnserializeStrIntMap(map, sz)) {
    return;
  }

  reserveForAdd(sz + sz); // keys + values
  for (int64_t i = 0; i < sz; ++i) {
    Variant k;
    unserializeVariant(k.asTypedValue(), UnserializeMode::ColKey);
    TypedValue* tv = nullptr;
    if (k.isInteger()) {
      auto h = k.toInt64();
      tv = map->findForUnserialize(h);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) {
        tv = k.asTypedValue();
        goto do_unserialize;
      }
    } else if (k.isString()) {
      auto key = k.getStringData();
      tv = map->findForUnserialize(key);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) {
        tv = k.asTypedValue();
        goto do_unserialize;
      }
    } else {
      throwInvalidKey();
    }
    tv->m_type = KindOfNull;
do_unserialize:
    unserializeVariant(tv);
  }
}

void VariableUnserializer::unserializeSet(ObjectData* obj, int64_t sz,
                                          char type) {
  if (type != 'V') throwBadFormat(obj, type);

  // For large maps, do a naive pre-check for OOM.
  auto const allocsz = MixedArray::computeAllocBytesFromMaxElms(sz);
  if (UNLIKELY(allocsz > kMaxSmallSize && tl_heap->preAllocOOM(allocsz))) {
    check_non_safepoint_surprise();
  }

  auto set = static_cast<BaseSet*>(obj);
  set->reserve(sz);

  reserveForAdd(sz);
  for (int64_t i = 0; i < sz; ++i) {
    // When unserializing an element of a Set, we use Mode::ColKey for now.
    // This will make the unserializer to reserve an id for the element
    // but won't allow referencing the element via 'r' or 'R'.
    Variant k;
    unserializeVariant(k.asTypedValue(), UnserializeMode::ColKey);
    if (k.isInteger()) {
      auto h = k.toInt64();
      auto tv = set->findForUnserialize(h);
      // Be robust against manually crafted inputs with conflicting elements
      if (UNLIKELY(!tv)) continue;
      tv->m_type = KindOfInt64;
      tv->m_data.num = h;
    } else if (k.isString()) {
      auto key = k.getStringData();
      auto tv = set->findForUnserialize(key);
      if (UNLIKELY(!tv)) continue;
      // This increments the string's refcount twice, once for
      // the key and once for the value
      tvDup(make_tv<KindOfString>(key), *tv);
    } else {
      throwInvalidHashKey(obj);
    }
  }
}

void VariableUnserializer::unserializePair(ObjectData* obj, int64_t sz,
                                           char type) {
  assertx(sz == 2);
  if (type != 'V') throwBadFormat(obj, type);
  auto pair = static_cast<c_Pair*>(obj);
  unserializeVariant(pair->at(0));
  unserializeVariant(pair->at(1));
}

////////////////////////////////////////////////////////////////////////////////

void VariableUnserializer::reserialize(StringBuffer& buf) {

  char type = readChar();
  char sep = readChar();

  if (type == 'N') {
    buf.append(type);
    buf.append(sep);
    return;
  }

  switch (type) {
  case 'r':
  case 'R':
  case 'b':
  case 'i':
  case 'd':
    {
      buf.append(type);
      buf.append(sep);
      while (peek() != ';') {
        char ch;
        ch = readChar();
        buf.append(ch);
      }
    }
    break;
  case 'S':
  case 'A':
    {
      // shouldn't happen, but keep the code here anyway.
      buf.append(type);
      buf.append(sep);
      auto str = readStr(8);
      buf.append(str.data(), str.size());
    }
    break;
  case 's':
    {
      String v = unserializeString();
      assertx(!v.isNull());
      if (v.get()->isStatic()) {
        union {
          char pointer[8];
          StringData *sd;
        } u;
        u.sd = v.get();
        buf.append("S:");
        buf.append(u.pointer, 8);
        buf.append(';');
      } else {
        buf.append("s:");
        buf.append(v.size());
        buf.append(":\"");
        buf.append(v.data(), v.size());
        buf.append("\";");
      }
      sep = readChar();
      return;
    }
    break;
  case 'a':
  case 'D':
  case 'Y':
  case 'H':
    {
      buf.append(type == 'a' ? "a:" : (type == 'Y' ? "Y:" :
            (type == 'D' ? "D:" : "H:")));
      int64_t size = readInt();
      char sep2 = readChar();
      buf.append(size);
      buf.append(sep2);
      sep2 = readChar();
      buf.append(sep2);
      for (int64_t i = 0; i < size; i++) {
        reserialize(buf); // key
        reserialize(buf); // value
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
    break;
  case 'v':
  case 'k':
  case 'y':
    {
      buf.append(type == 'v' ? "v:" : (type == 'y' ? "y:" : "k:"));
      int64_t size = readInt();
      char sep2 = readChar();
      buf.append(size);
      buf.append(sep2);
      sep2 = readChar();
      buf.append(sep2);
      for (int64_t i = 0; i < size; ++i) {
        reserialize(buf);
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
  case 'o':
  case 'O':
  case 'V':
  case 'K':
    {
      buf.append(type);
      buf.append(sep);

      auto const clsName = unserializeStringPiece();
      buf.append(static_cast<int>(clsName.size()));
      buf.append(":\"");
      buf.append(clsName.data(), clsName.size());
      buf.append("\":");

      readChar();
      int64_t size = readInt();
      char sep2 = readChar();

      buf.append(size);
      buf.append(sep2);
      sep2 = readChar(); // '{'
      buf.append(sep2);
      // 'V' type is a series with values only, while all other
      // types are series with keys and values
      int64_t i = type == 'V' ? size : size * 2;
      while (i--) {
        reserialize(buf);
      }
      sep2 = readChar(); // '}'
      buf.append(sep2);
      return;
    }
    break;
  case 'C':
    {
      buf.append(type);
      buf.append(sep);

      auto const clsName = unserializeStringPiece();
      buf.append(static_cast<int>(clsName.size()));
      buf.append(":\"");
      buf.append(clsName.data(), clsName.size());
      buf.append("\":");

      sep = readChar(); // ':'
      auto const serialized = unserializeStringPiece('{', '}');
      buf.append(static_cast<int>(serialized.size()));
      buf.append(":{");
      buf.append(serialized.data(), serialized.size());
      buf.append('}');
      return;
    }
    break;
  default:
    throwUnknownType(type);
  }

  sep = readChar(); // the last ';'
  buf.append(sep);
}

}