1/*
2 * Copyright 2016-present Facebook, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16/*
17 * Copyright (c) 2015, Facebook, Inc.
18 * All rights reserved.
19 *
20 * This source code is licensed under the BSD-style license found in the
21 * LICENSE file in the root directory of this source tree. An additional grant
22 * of patent rights can be found in the PATENTS file in the same directory.
23 *
24 */
25#include <folly/experimental/DynamicParser.h>
26
27#include <sstream>
28
29#include <folly/Optional.h>
30
31namespace folly {
32
33namespace {
34folly::dynamic& insertAtKey(
35 folly::dynamic* d,
36 bool allow_non_string_keys,
37 const folly::dynamic& key) {
38 if (key.isString()) {
39 return (*d)[key];
40 } else if (key.isNumber() || key.isBool()) {
41 // folly::dynamic allows non-null scalars for keys.
42 return allow_non_string_keys ? (*d)[key] : (*d)[key.asString()];
43 }
44 // One cause might be oddness like p.optional(dynamic::array(...), ...);
45 throw DynamicParserLogicError(
46 "Unsupported key type ",
47 key.typeName(),
48 " of ",
49 detail::toPseudoJson(key));
50}
51} // namespace
52
53void DynamicParser::reportError(
54 const folly::dynamic* lookup_k,
55 const std::exception& ex) {
56 // If descendants of this item, or other keys on it, already reported an
57 // error, the error object would already exist.
58 auto& e = stack_.errors(allowNonStringKeyErrors_);
59
60 // Save the original, unparseable value of the item causing the error.
61 //
62 // value() can throw here, but if it does, it is due to programmer error,
63 // so we don't want to report it as a parse error anyway.
64 if (auto* e_val_ptr = e.get_ptr("value")) {
65 // Failing to access distinct keys on the same value can generate
66 // multiple errors, but the value should remain the same.
67 if (*e_val_ptr != value()) {
68 throw DynamicParserLogicError(
69 "Overwriting value: ",
70 detail::toPseudoJson(*e_val_ptr),
71 " with ",
72 detail::toPseudoJson(value()),
73 " for error ",
74 ex.what());
75 }
76 } else {
77 // The e["value"].isNull() trick cannot be used because value().type()
78 // *can* be folly::dynamic::Type::NULLT, so we must hash again.
79 e["value"] = value();
80 }
81
82 // Differentiate between "parsing value" and "looking up key" errors.
83 auto& e_msg = [&]() -> folly::dynamic& {
84 if (lookup_k == nullptr) { // {object,array}Items, or post-key-lookup
85 return e["error"];
86 }
87 // Multiple key lookups can report errors on the same collection.
88 auto& key_errors = e["key_errors"];
89 if (key_errors.isNull()) {
90 // Treat arrays as integer-keyed objects.
91 key_errors = folly::dynamic::object();
92 }
93 return insertAtKey(&key_errors, allowNonStringKeyErrors_, *lookup_k);
94 }();
95 if (!e_msg.isNull()) {
96 throw DynamicParserLogicError(
97 "Overwriting error: ",
98 detail::toPseudoJson(e_msg),
99 " with: ",
100 ex.what());
101 }
102 e_msg = ex.what();
103
104 switch (onError_) {
105 case OnError::RECORD:
106 break; // Continue parsing
107 case OnError::THROW:
108 stack_.throwErrors(); // Package releaseErrors() into an exception.
109 default:
110 LOG(FATAL) << "Bad onError_: " << static_cast<int>(onError_);
111 }
112}
113
114void DynamicParser::ParserStack::Pop::operator()() noexcept {
115 stackPtr_->key_ = key_;
116 stackPtr_->value_ = value_;
117 if (stackPtr_->unmaterializedSubErrorKeys_.empty()) {
118 // There should be the current error, and the root.
119 CHECK_GE(stackPtr_->subErrors_.size(), 2u)
120 << "Internal bug: out of suberrors";
121 stackPtr_->subErrors_.pop_back();
122 } else {
123 // Errors were never materialized for this subtree, so errors_ only has
124 // ancestors of the item being processed.
125 stackPtr_->unmaterializedSubErrorKeys_.pop_back();
126 CHECK(!stackPtr_->subErrors_.empty()) << "Internal bug: out of suberrors";
127 }
128}
129
130DynamicParser::ParserStack::PopGuard DynamicParser::ParserStack::push(
131 const folly::dynamic& k,
132 const folly::dynamic& v) noexcept {
133 // Save the previous state of the parser.
134 DynamicParser::ParserStack::PopGuard guard{this};
135 key_ = &k;
136 value_ = &v;
137 // We create errors_ sub-objects lazily to keep the result small.
138 unmaterializedSubErrorKeys_.emplace_back(key_);
139 return guard;
140}
141
142// `noexcept` because if the materialization loop threw, we'd end up with
143// more suberrors than we started with.
144folly::dynamic& DynamicParser::ParserStack::errors(
145 bool allow_non_string_keys) noexcept {
146 // Materialize the lazy "key + parent's type" error objects we'll need.
147 CHECK(!subErrors_.empty()) << "Internal bug: out of suberrors";
148 for (const auto& suberror_key : unmaterializedSubErrorKeys_) {
149 auto& nested = (*subErrors_.back())["nested"];
150 if (nested.isNull()) {
151 nested = folly::dynamic::object();
152 }
153 // Find, or insert a dummy entry for the current key
154 auto& my_errors =
155 insertAtKey(&nested, allow_non_string_keys, *suberror_key);
156 if (my_errors.isNull()) {
157 my_errors = folly::dynamic::object();
158 }
159 subErrors_.emplace_back(&my_errors);
160 }
161 unmaterializedSubErrorKeys_.clear();
162 return *subErrors_.back();
163}
164
165folly::dynamic DynamicParser::ParserStack::releaseErrors() {
166 if (key_ || unmaterializedSubErrorKeys_.size() != 0 ||
167 subErrors_.size() != 1) {
168 throw DynamicParserLogicError(
169 "Do not releaseErrors() while parsing: ",
170 key_ != nullptr,
171 " / ",
172 unmaterializedSubErrorKeys_.size(),
173 " / ",
174 subErrors_.size());
175 }
176 return releaseErrorsImpl();
177}
178
179[[noreturn]] void DynamicParser::ParserStack::throwErrors() {
180 throw DynamicParserParseError(releaseErrorsImpl());
181}
182
183folly::dynamic DynamicParser::ParserStack::releaseErrorsImpl() {
184 if (errors_.isNull()) {
185 throw DynamicParserLogicError("Do not releaseErrors() twice");
186 }
187 auto errors = std::move(errors_);
188 errors_ = nullptr; // Prevent a second release.
189 value_ = nullptr; // Break attempts to parse again.
190 return errors;
191}
192
193namespace detail {
194std::string toPseudoJson(const folly::dynamic& d) {
195 std::stringstream ss;
196 ss << d;
197 return ss.str();
198}
199} // namespace detail
200
201} // namespace folly
202