Lalit Maganti | b07532d | 2021-07-01 22:13:57 +0100 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2021 The Android Open Source Project |
| 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 | #include "tools/proto_merger/proto_merger.h" |
| 18 | |
| 19 | #include "perfetto/base/logging.h" |
| 20 | #include "perfetto/base/status.h" |
| 21 | #include "perfetto/ext/base/optional.h" |
| 22 | |
| 23 | namespace perfetto { |
| 24 | namespace proto_merger { |
| 25 | namespace { |
| 26 | |
| 27 | template <typename Key, typename Value> |
| 28 | base::Optional<Value> FindInMap(const std::map<Key, Value>& map, |
| 29 | const Key& key) { |
| 30 | auto it = map.find(key); |
| 31 | return it == map.end() ? base::nullopt : base::make_optional(it->second); |
| 32 | } |
| 33 | |
| 34 | // Finds the given 'name' in the vector by comparing against |
| 35 | // the field named 'name' for each item in the vector. |
Lalit Maganti | dade39c | 2021-07-08 19:30:49 +0100 | [diff] [blame] | 36 | // T is ProtoFile::Enum, ProtoFile::Oneof or ProtoFile::Message. |
Lalit Maganti | b07532d | 2021-07-01 22:13:57 +0100 | [diff] [blame] | 37 | template <typename T> |
| 38 | const T* FindByName(const std::vector<T>& items, const std::string& name) { |
| 39 | for (const auto& item : items) { |
| 40 | if (item.name == name) |
| 41 | return &item; |
| 42 | } |
| 43 | return nullptr; |
| 44 | } |
| 45 | |
| 46 | // Compute the items present in the |input| vector but deleted in |
| 47 | // the |upstream| vector by looking at the field |name|. |
Lalit Maganti | dade39c | 2021-07-08 19:30:49 +0100 | [diff] [blame] | 48 | // T is ProtoFile::Enum, ProtoFile::Oneof or ProtoFile::Message. |
Lalit Maganti | b07532d | 2021-07-01 22:13:57 +0100 | [diff] [blame] | 49 | template <typename T> |
| 50 | std::vector<T> ComputeDeletedByName(const std::vector<T>& input, |
| 51 | const std::vector<T>& upstream) { |
| 52 | std::vector<T> deleted; |
| 53 | std::set<std::string> seen; |
| 54 | for (const auto& upstream_item : upstream) { |
| 55 | auto* input_item = FindByName(input, upstream_item.name); |
| 56 | if (!input_item) |
| 57 | continue; |
| 58 | seen.insert(input_item->name); |
| 59 | } |
| 60 | |
| 61 | for (const auto& input_item : input) { |
| 62 | if (seen.count(input_item.name)) |
| 63 | continue; |
| 64 | deleted.emplace_back(input_item); |
| 65 | } |
| 66 | return deleted; |
| 67 | } |
| 68 | |
| 69 | // Finds the given 'number' in the vector by comparing against |
| 70 | // the field named 'number for each item in the vector. |
Lalit Maganti | dade39c | 2021-07-08 19:30:49 +0100 | [diff] [blame] | 71 | // T is ProtoFile::EnumValue or ProtoFile::Field. |
Lalit Maganti | b07532d | 2021-07-01 22:13:57 +0100 | [diff] [blame] | 72 | template <typename T> |
| 73 | const T* FindByNumber(const std::vector<T>& items, int number) { |
| 74 | for (const auto& item : items) { |
| 75 | if (item.number == number) |
| 76 | return &item; |
| 77 | } |
| 78 | return nullptr; |
| 79 | } |
| 80 | |
| 81 | // Compute the items present in the |input| vector but deleted in |
| 82 | // the |upstream| vector by looking at the field |number|. |
Lalit Maganti | dade39c | 2021-07-08 19:30:49 +0100 | [diff] [blame] | 83 | // T is ProtoFile::EnumValue or ProtoFile::Field. |
Lalit Maganti | b07532d | 2021-07-01 22:13:57 +0100 | [diff] [blame] | 84 | template <typename T> |
| 85 | std::vector<T> ComputeDeletedByNumber(const std::vector<T>& input, |
| 86 | const std::vector<T>& upstream) { |
| 87 | std::vector<T> deleted; |
| 88 | std::set<int> seen; |
| 89 | for (const auto& upstream_item : upstream) { |
| 90 | auto* input_item = FindByNumber(input, upstream_item.number); |
| 91 | if (!input_item) |
| 92 | continue; |
| 93 | seen.insert(input_item->number); |
| 94 | } |
| 95 | |
| 96 | for (const auto& input_item : input) { |
| 97 | if (seen.count(input_item.number)) |
| 98 | continue; |
| 99 | deleted.emplace_back(input_item); |
| 100 | } |
| 101 | return deleted; |
| 102 | } |
| 103 | |
| 104 | ProtoFile::Enum::Value MergeEnumValue(const ProtoFile::Enum::Value& input, |
| 105 | const ProtoFile::Enum::Value& upstream) { |
| 106 | PERFETTO_CHECK(input.number == upstream.number); |
| 107 | |
| 108 | ProtoFile::Enum::Value out; |
| 109 | out.name = upstream.name; |
| 110 | |
| 111 | // Get the comments from the source of truth. |
| 112 | out.leading_comments = upstream.leading_comments; |
| 113 | out.trailing_comments = upstream.trailing_comments; |
| 114 | |
| 115 | // Get everything else from the input. |
| 116 | out.number = input.number; |
| 117 | out.options = input.options; |
| 118 | return out; |
| 119 | } |
| 120 | |
| 121 | ProtoFile::Enum MergeEnum(const ProtoFile::Enum& input, |
| 122 | const ProtoFile::Enum& upstream) { |
| 123 | PERFETTO_CHECK(input.name == upstream.name); |
| 124 | |
| 125 | ProtoFile::Enum out; |
| 126 | out.name = upstream.name; |
| 127 | |
| 128 | // Get the comments from the source of truth. |
| 129 | out.leading_comments = upstream.leading_comments; |
| 130 | out.trailing_comments = upstream.trailing_comments; |
| 131 | |
| 132 | for (const auto& upstream_value : upstream.values) { |
| 133 | // If an enum is allowlisted, we implicitly assume that all its |
| 134 | // values are also allowed. Therefore, if the value doesn't exist |
| 135 | // in the input, just take it from the source of truth. |
| 136 | auto* input_value = FindByNumber(input.values, upstream_value.number); |
| 137 | auto out_value = input_value ? MergeEnumValue(*input_value, upstream_value) |
| 138 | : upstream_value; |
| 139 | out.values.emplace_back(std::move(out_value)); |
| 140 | } |
| 141 | |
| 142 | // Compute all the values present in the input but deleted in the |
| 143 | // source of truth. |
| 144 | out.deleted_values = ComputeDeletedByNumber(input.values, upstream.values); |
| 145 | return out; |
| 146 | } |
| 147 | |
| 148 | std::vector<ProtoFile::Enum> MergeEnums( |
| 149 | const std::vector<ProtoFile::Enum>& input, |
| 150 | const std::vector<ProtoFile::Enum>& upstream, |
| 151 | const std::set<std::string>& allowlist) { |
| 152 | std::vector<ProtoFile::Enum> out; |
| 153 | for (const auto& upstream_enum : upstream) { |
| 154 | auto* input_enum = FindByName(input, upstream_enum.name); |
| 155 | if (!input_enum) { |
| 156 | // If the enum is missing from the input but is present |
| 157 | // in the allowlist, take the whole enum from the |
| 158 | // source of truth. |
| 159 | if (allowlist.count(upstream_enum.name)) |
| 160 | out.emplace_back(upstream_enum); |
| 161 | continue; |
| 162 | } |
| 163 | |
| 164 | // Otherwise, merge the enums from the input and source of truth. |
| 165 | out.emplace_back(MergeEnum(*input_enum, upstream_enum)); |
| 166 | } |
| 167 | return out; |
| 168 | } |
| 169 | |
| 170 | base::Status MergeField(const ProtoFile::Field& input, |
| 171 | const ProtoFile::Field& upstream, |
| 172 | ProtoFile::Field& out) { |
| 173 | PERFETTO_CHECK(input.number == upstream.number); |
| 174 | |
| 175 | if (input.packageless_type != upstream.packageless_type) { |
| 176 | return base::ErrStatus( |
| 177 | "The type of field with id %d and name %s (source of truth name: %s) " |
| 178 | "changed from %s to %s. Please resolve conflict manually before " |
| 179 | "rerunning.", |
| 180 | input.number, input.name.c_str(), upstream.name.c_str(), |
| 181 | input.packageless_type.c_str(), upstream.packageless_type.c_str()); |
| 182 | } |
| 183 | |
| 184 | // If the packageless type mathces, the type should also match. |
| 185 | PERFETTO_CHECK(input.type == upstream.type); |
| 186 | |
| 187 | // Get the comments, label and the name from the source of truth. |
| 188 | out.leading_comments = upstream.leading_comments; |
| 189 | out.trailing_comments = upstream.trailing_comments; |
| 190 | out.label = upstream.label; |
| 191 | out.name = upstream.name; |
| 192 | |
| 193 | // Get everything else from the input. |
| 194 | out.number = input.number; |
| 195 | out.options = input.options; |
| 196 | out.packageless_type = input.packageless_type; |
| 197 | out.type = input.type; |
| 198 | |
| 199 | return base::OkStatus(); |
| 200 | } |
| 201 | |
| 202 | base::Status MergeFields(const std::vector<ProtoFile::Field>& input, |
| 203 | const std::vector<ProtoFile::Field>& upstream, |
| 204 | const std::set<int>& allowlist, |
| 205 | std::vector<ProtoFile::Field>& out) { |
| 206 | for (const auto& upstream_field : upstream) { |
| 207 | auto* input_field = FindByNumber(input, upstream_field.number); |
| 208 | if (!input_field) { |
| 209 | // If the field is missing from the input but is present |
| 210 | // in the allowlist, take the whole field from the |
| 211 | // source of truth. |
| 212 | if (allowlist.count(upstream_field.number)) |
| 213 | out.emplace_back(upstream_field); |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | // Otherwise, merge the fields from the input and source of truth. |
| 218 | ProtoFile::Field out_field; |
| 219 | base::Status status = MergeField(*input_field, upstream_field, out_field); |
| 220 | if (!status.ok()) |
| 221 | return status; |
| 222 | out.emplace_back(std::move(out_field)); |
| 223 | } |
| 224 | return base::OkStatus(); |
| 225 | } |
| 226 | |
| 227 | // We call both of these just "Merge" so that |MergeRecursive| below can |
| 228 | // reference them with the same name. |
| 229 | base::Status Merge(const ProtoFile::Oneof& input, |
| 230 | const ProtoFile::Oneof& upstream, |
| 231 | const Allowlist::Oneof& allowlist, |
| 232 | ProtoFile::Oneof& out); |
| 233 | |
| 234 | base::Status Merge(const ProtoFile::Message& input, |
| 235 | const ProtoFile::Message& upstream, |
| 236 | const Allowlist::Message& allowlist, |
| 237 | ProtoFile::Message& out); |
| 238 | |
| 239 | template <typename T, typename AllowlistType> |
| 240 | base::Status MergeRecursive( |
| 241 | const std::vector<T>& input, |
| 242 | const std::vector<T>& upstream, |
| 243 | const std::map<std::string, AllowlistType>& allowlist_map, |
| 244 | std::vector<T>& out) { |
| 245 | for (const auto& upstream_item : upstream) { |
| 246 | auto opt_allowlist = FindInMap(allowlist_map, upstream_item.name); |
| 247 | auto* input_item = FindByName(input, upstream_item.name); |
| 248 | |
| 249 | // If the value is not present in the input and the allowlist doesn't |
| 250 | // exist either, this field is not approved so should not be included |
| 251 | // in the output. |
| 252 | if (!input_item && !opt_allowlist) |
| 253 | continue; |
| 254 | |
| 255 | // If the input value doesn't exist, create a fake "input" that we can pass |
| 256 | // to the merge functon. This basically has the effect that the upstream |
| 257 | // item is taken but *not* recrusively; i.e. any fields which are inside the |
| 258 | // message/oneof are checked against the allowlist individually. If we just |
| 259 | // took the whole upstream here, we could add fields which were not |
| 260 | // allowlisted. |
| 261 | T input_or_fake; |
| 262 | if (input_item) { |
| 263 | input_or_fake = *input_item; |
| 264 | } else { |
| 265 | input_or_fake.name = upstream_item.name; |
| 266 | } |
| 267 | |
| 268 | auto allowlist = opt_allowlist.value_or(AllowlistType{}); |
| 269 | T out_item; |
| 270 | auto status = Merge(input_or_fake, upstream_item, allowlist, out_item); |
| 271 | if (!status.ok()) |
| 272 | return status; |
| 273 | out.emplace_back(std::move(out_item)); |
| 274 | } |
| 275 | return base::OkStatus(); |
| 276 | } |
| 277 | |
| 278 | base::Status Merge(const ProtoFile::Oneof& input, |
| 279 | const ProtoFile::Oneof& upstream, |
| 280 | const Allowlist::Oneof& allowlist, |
| 281 | ProtoFile::Oneof& out) { |
| 282 | PERFETTO_CHECK(input.name == upstream.name); |
| 283 | out.name = input.name; |
| 284 | |
| 285 | // Get the comments from the source of truth. |
| 286 | out.leading_comments = upstream.leading_comments; |
| 287 | out.trailing_comments = upstream.trailing_comments; |
| 288 | |
| 289 | // Compute all the fields present in the input but deleted in the |
| 290 | // source of truth. |
| 291 | out.deleted_fields = ComputeDeletedByNumber(input.fields, upstream.fields); |
| 292 | |
| 293 | // Finish by merging the list of fields. |
| 294 | return MergeFields(input.fields, upstream.fields, allowlist, out.fields); |
| 295 | } |
| 296 | |
| 297 | base::Status Merge(const ProtoFile::Message& input, |
| 298 | const ProtoFile::Message& upstream, |
| 299 | const Allowlist::Message& allowlist, |
| 300 | ProtoFile::Message& out) { |
| 301 | PERFETTO_CHECK(input.name == upstream.name); |
| 302 | out.name = input.name; |
| 303 | |
| 304 | // Get the comments from the source of truth. |
| 305 | out.leading_comments = upstream.leading_comments; |
| 306 | out.trailing_comments = upstream.trailing_comments; |
| 307 | |
| 308 | // Compute all the values present in the input but deleted in the |
| 309 | // source of truth. |
| 310 | out.deleted_enums = ComputeDeletedByName(input.enums, upstream.enums); |
| 311 | out.deleted_nested_messages = |
| 312 | ComputeDeletedByName(input.nested_messages, upstream.nested_messages); |
| 313 | out.deleted_oneofs = ComputeDeletedByName(input.oneofs, upstream.oneofs); |
| 314 | out.deleted_fields = ComputeDeletedByNumber(input.fields, upstream.fields); |
| 315 | |
| 316 | // Merge any nested enum types. |
| 317 | out.enums = MergeEnums(input.enums, upstream.enums, allowlist.enums); |
| 318 | |
| 319 | // Merge any nested message types. |
| 320 | auto status = MergeRecursive(input.nested_messages, upstream.nested_messages, |
| 321 | allowlist.nested_messages, out.nested_messages); |
| 322 | if (!status.ok()) |
| 323 | return status; |
| 324 | |
| 325 | // Merge any oneofs. |
| 326 | status = MergeRecursive(input.oneofs, upstream.oneofs, allowlist.oneofs, |
| 327 | out.oneofs); |
| 328 | if (!status.ok()) |
| 329 | return status; |
| 330 | |
| 331 | // Finish by merging the list of fields. |
| 332 | return MergeFields(input.fields, upstream.fields, allowlist.fields, |
| 333 | out.fields); |
| 334 | } |
| 335 | |
| 336 | } // namespace |
| 337 | |
| 338 | base::Status MergeProtoFiles(const ProtoFile& input, |
| 339 | const ProtoFile& upstream, |
| 340 | const Allowlist& allowlist, |
| 341 | ProtoFile& out) { |
| 342 | // Compute all the enums and messages present in the input but deleted in the |
| 343 | // source of truth. |
| 344 | out.deleted_enums = ComputeDeletedByName(input.enums, upstream.enums); |
| 345 | out.deleted_messages = |
| 346 | ComputeDeletedByName(input.messages, upstream.messages); |
| 347 | |
| 348 | // Merge the top-level enums. |
| 349 | out.enums = MergeEnums(input.enums, upstream.enums, allowlist.enums); |
| 350 | |
| 351 | // Finish by merging the top-level messages. |
| 352 | return MergeRecursive(input.messages, upstream.messages, allowlist.messages, |
| 353 | out.messages); |
| 354 | } |
| 355 | |
| 356 | } // namespace proto_merger |
| 357 | } // namespace perfetto |