1 // Copyright (C) 2019 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "icing/schema/schema-util.h"
16
17 #include <cstdint>
18 #include <string>
19 #include <string_view>
20 #include <unordered_map>
21 #include <unordered_set>
22 #include <utility>
23
24 #include "icing/text_classifier/lib3/utils/base/status.h"
25 #include "icing/absl_ports/annotate.h"
26 #include "icing/absl_ports/canonical_errors.h"
27 #include "icing/absl_ports/str_cat.h"
28 #include "icing/absl_ports/str_join.h"
29 #include "icing/legacy/core/icing-string-util.h"
30 #include "icing/proto/schema.pb.h"
31 #include "icing/proto/term.pb.h"
32 #include "icing/util/logging.h"
33 #include "icing/util/status-macros.h"
34
35 namespace icing {
36 namespace lib {
37
38 namespace {
39
IsCardinalityCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)40 bool IsCardinalityCompatible(const PropertyConfigProto& old_property,
41 const PropertyConfigProto& new_property) {
42 if (old_property.cardinality() < new_property.cardinality()) {
43 // We allow a new, less restrictive cardinality (i.e. a REQUIRED field
44 // can become REPEATED or OPTIONAL, but not the other way around).
45 ICING_VLOG(1) << absl_ports::StrCat(
46 "Cardinality is more restrictive than before ",
47 PropertyConfigProto::Cardinality::Code_Name(old_property.cardinality()),
48 "->",
49 PropertyConfigProto::Cardinality::Code_Name(
50 new_property.cardinality()));
51 return false;
52 }
53 return true;
54 }
55
IsDataTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)56 bool IsDataTypeCompatible(const PropertyConfigProto& old_property,
57 const PropertyConfigProto& new_property) {
58 if (old_property.data_type() != new_property.data_type()) {
59 // TODO(cassiewang): Maybe we can be a bit looser with this, e.g. we just
60 // string cast an int64_t to a string. But for now, we'll stick with
61 // simplistics.
62 ICING_VLOG(1) << absl_ports::StrCat(
63 "Data type ",
64 PropertyConfigProto::DataType::Code_Name(old_property.data_type()),
65 "->",
66 PropertyConfigProto::DataType::Code_Name(new_property.data_type()));
67 return false;
68 }
69 return true;
70 }
71
IsSchemaTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)72 bool IsSchemaTypeCompatible(const PropertyConfigProto& old_property,
73 const PropertyConfigProto& new_property) {
74 if (old_property.schema_type() != new_property.schema_type()) {
75 ICING_VLOG(1) << absl_ports::StrCat("Schema type ",
76 old_property.schema_type(), "->",
77 new_property.schema_type());
78 return false;
79 }
80 return true;
81 }
82
IsPropertyCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)83 bool IsPropertyCompatible(const PropertyConfigProto& old_property,
84 const PropertyConfigProto& new_property) {
85 return IsDataTypeCompatible(old_property, new_property) &&
86 IsSchemaTypeCompatible(old_property, new_property) &&
87 IsCardinalityCompatible(old_property, new_property);
88 }
89
IsTermMatchTypeCompatible(const StringIndexingConfig & old_indexed,const StringIndexingConfig & new_indexed)90 bool IsTermMatchTypeCompatible(const StringIndexingConfig& old_indexed,
91 const StringIndexingConfig& new_indexed) {
92 return old_indexed.term_match_type() == new_indexed.term_match_type() &&
93 old_indexed.tokenizer_type() == new_indexed.tokenizer_type();
94 }
95
96 } // namespace
97
ExpandTranstiveDependencies(const SchemaUtil::DependencyMap & child_to_direct_parent_map,std::string_view type,SchemaUtil::DependencyMap * expanded_child_to_parent_map,std::unordered_set<std::string_view> * pending_expansions,std::unordered_set<std::string_view> * orphaned_types)98 libtextclassifier3::Status ExpandTranstiveDependencies(
99 const SchemaUtil::DependencyMap& child_to_direct_parent_map,
100 std::string_view type,
101 SchemaUtil::DependencyMap* expanded_child_to_parent_map,
102 std::unordered_set<std::string_view>* pending_expansions,
103 std::unordered_set<std::string_view>* orphaned_types) {
104 auto expanded_itr = expanded_child_to_parent_map->find(type);
105 if (expanded_itr != expanded_child_to_parent_map->end()) {
106 // We've already expanded this type. Just return.
107 return libtextclassifier3::Status::OK;
108 }
109 auto itr = child_to_direct_parent_map.find(type);
110 if (itr == child_to_direct_parent_map.end()) {
111 // It's an orphan. Just return.
112 orphaned_types->insert(type);
113 return libtextclassifier3::Status::OK;
114 }
115 pending_expansions->insert(type);
116 std::unordered_set<std::string_view> expanded_dependencies;
117
118 // Add all of the direct parent dependencies.
119 expanded_dependencies.reserve(itr->second.size());
120 expanded_dependencies.insert(itr->second.begin(), itr->second.end());
121
122 // Iterate through each direct parent and add their indirect parents.
123 for (std::string_view dep : itr->second) {
124 // 1. Check if we're in the middle of expanding this type - IOW there's a
125 // cycle!
126 if (pending_expansions->count(dep) > 0) {
127 return absl_ports::InvalidArgumentError(
128 absl_ports::StrCat("Infinite loop detected in type configs. '", type,
129 "' references itself."));
130 }
131
132 // 2. Expand this type as needed.
133 ICING_RETURN_IF_ERROR(ExpandTranstiveDependencies(
134 child_to_direct_parent_map, dep, expanded_child_to_parent_map,
135 pending_expansions, orphaned_types));
136 if (orphaned_types->count(dep) > 0) {
137 // Dep is an orphan. Just skip to the next dep.
138 continue;
139 }
140
141 // 3. Dep has been fully expanded. Add all of its dependencies to this
142 // type's dependencies.
143 auto dep_expanded_itr = expanded_child_to_parent_map->find(dep);
144 expanded_dependencies.reserve(expanded_dependencies.size() +
145 dep_expanded_itr->second.size());
146 expanded_dependencies.insert(dep_expanded_itr->second.begin(),
147 dep_expanded_itr->second.end());
148 }
149 expanded_child_to_parent_map->insert(
150 {type, std::move(expanded_dependencies)});
151 pending_expansions->erase(type);
152 return libtextclassifier3::Status::OK;
153 }
154
155 // Expands the dependencies represented by the child_to_direct_parent_map to
156 // also include indirect parents.
157 //
158 // Ex. Suppose we have a schema with four types A, B, C, D. A has a property of
159 // type B and B has a property of type C. C and D only have non-document
160 // properties.
161 //
162 // The child to direct parent dependency map for this schema would be:
163 // C -> B
164 // B -> A
165 //
166 // This function would expand it so that A is also present as an indirect parent
167 // of C.
168 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap>
ExpandTranstiveDependencies(const SchemaUtil::DependencyMap & child_to_direct_parent_map)169 ExpandTranstiveDependencies(
170 const SchemaUtil::DependencyMap& child_to_direct_parent_map) {
171 SchemaUtil::DependencyMap expanded_child_to_parent_map;
172
173 // Types that we are expanding.
174 std::unordered_set<std::string_view> pending_expansions;
175
176 // Types that have no parents that depend on them.
177 std::unordered_set<std::string_view> orphaned_types;
178 for (const auto& kvp : child_to_direct_parent_map) {
179 ICING_RETURN_IF_ERROR(ExpandTranstiveDependencies(
180 child_to_direct_parent_map, kvp.first, &expanded_child_to_parent_map,
181 &pending_expansions, &orphaned_types));
182 }
183 return expanded_child_to_parent_map;
184 }
185
186 // Builds a transitive child-parent dependency map. 'Orphaned' types (types with
187 // no parents) will not be present in the map.
188 //
189 // Ex. Suppose we have a schema with four types A, B, C, D. A has a property of
190 // type B and B has a property of type C. C and D only have non-document
191 // properties.
192 //
193 // The transitive child-parent dependency map for this schema would be:
194 // C -> A, B
195 // B -> A
196 //
197 // A and D would be considered orphaned properties because no type refers to
198 // them.
199 //
200 // RETURNS:
201 // On success, a transitive child-parent dependency map of all types in the
202 // schema.
203 // INVALID_ARGUMENT if the schema contains a cycle or an undefined type.
204 // ALREADY_EXISTS if a schema type is specified more than once in the schema
205 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap>
BuildTransitiveDependencyGraph(const SchemaProto & schema)206 BuildTransitiveDependencyGraph(const SchemaProto& schema) {
207 // Child to parent map.
208 SchemaUtil::DependencyMap child_to_direct_parent_map;
209
210 // Add all first-order dependencies.
211 std::unordered_set<std::string_view> known_types;
212 std::unordered_set<std::string_view> unknown_types;
213 for (const auto& type_config : schema.types()) {
214 std::string_view schema_type(type_config.schema_type());
215 if (known_types.count(schema_type) > 0) {
216 return absl_ports::AlreadyExistsError(absl_ports::StrCat(
217 "Field 'schema_type' '", schema_type, "' is already defined"));
218 }
219 known_types.insert(schema_type);
220 unknown_types.erase(schema_type);
221 for (const auto& property_config : type_config.properties()) {
222 if (property_config.data_type() ==
223 PropertyConfigProto::DataType::DOCUMENT) {
224 // Need to know what schema_type these Document properties should be
225 // validated against
226 std::string_view property_schema_type(property_config.schema_type());
227 if (property_schema_type == schema_type) {
228 return absl_ports::InvalidArgumentError(
229 absl_ports::StrCat("Infinite loop detected in type configs. '",
230 schema_type, "' references itself."));
231 }
232 if (known_types.count(property_schema_type) == 0) {
233 unknown_types.insert(property_schema_type);
234 }
235 auto itr = child_to_direct_parent_map.find(property_schema_type);
236 if (itr == child_to_direct_parent_map.end()) {
237 child_to_direct_parent_map.insert(
238 {property_schema_type, std::unordered_set<std::string_view>()});
239 itr = child_to_direct_parent_map.find(property_schema_type);
240 }
241 itr->second.insert(schema_type);
242 }
243 }
244 }
245 if (!unknown_types.empty()) {
246 return absl_ports::InvalidArgumentError(absl_ports::StrCat(
247 "Undefined 'schema_type's: ", absl_ports::StrJoin(unknown_types, ",")));
248 }
249 return ExpandTranstiveDependencies(child_to_direct_parent_map);
250 }
251
Validate(const SchemaProto & schema)252 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap> SchemaUtil::Validate(
253 const SchemaProto& schema) {
254 // 1. Build the dependency map. This will detect any cycles, non-existent or
255 // duplicate types in the schema.
256 ICING_ASSIGN_OR_RETURN(SchemaUtil::DependencyMap dependency_map,
257 BuildTransitiveDependencyGraph(schema));
258
259 // Tracks PropertyConfigs within a SchemaTypeConfig that we've validated
260 // already.
261 std::unordered_set<std::string_view> known_property_names;
262
263 // 2. Validate the properties of each type.
264 for (const auto& type_config : schema.types()) {
265 std::string_view schema_type(type_config.schema_type());
266 ICING_RETURN_IF_ERROR(ValidateSchemaType(schema_type));
267
268 // We only care about properties being unique within one type_config
269 known_property_names.clear();
270
271 for (const auto& property_config : type_config.properties()) {
272 std::string_view property_name(property_config.property_name());
273 ICING_RETURN_IF_ERROR(ValidatePropertyName(property_name, schema_type));
274
275 // Property names must be unique
276 if (!known_property_names.insert(property_name).second) {
277 return absl_ports::AlreadyExistsError(absl_ports::StrCat(
278 "Field 'property_name' '", property_name,
279 "' is already defined for schema '", schema_type, "'"));
280 }
281
282 auto data_type = property_config.data_type();
283 ICING_RETURN_IF_ERROR(
284 ValidateDataType(data_type, schema_type, property_name));
285
286 if (data_type == PropertyConfigProto::DataType::DOCUMENT) {
287 // Need to know what schema_type these Document properties should be
288 // validated against
289 std::string_view property_schema_type(property_config.schema_type());
290 libtextclassifier3::Status validated_status =
291 ValidateSchemaType(property_schema_type);
292 if (!validated_status.ok()) {
293 return absl_ports::Annotate(
294 validated_status,
295 absl_ports::StrCat("Field 'schema_type' is required for DOCUMENT "
296 "data_types in schema property '",
297 schema_type, ".", property_name, "'"));
298 }
299 }
300
301 ICING_RETURN_IF_ERROR(ValidateCardinality(property_config.cardinality(),
302 schema_type, property_name));
303
304 if (data_type == PropertyConfigProto::DataType::STRING) {
305 ICING_RETURN_IF_ERROR(ValidateStringIndexingConfig(
306 property_config.string_indexing_config(), data_type, schema_type,
307 property_name));
308 }
309 }
310 }
311
312 return dependency_map;
313 }
314
ValidateSchemaType(std::string_view schema_type)315 libtextclassifier3::Status SchemaUtil::ValidateSchemaType(
316 std::string_view schema_type) {
317 // Require a schema_type
318 if (schema_type.empty()) {
319 return absl_ports::InvalidArgumentError(
320 "Field 'schema_type' cannot be empty.");
321 }
322
323 return libtextclassifier3::Status::OK;
324 }
325
ValidatePropertyName(std::string_view property_name,std::string_view schema_type)326 libtextclassifier3::Status SchemaUtil::ValidatePropertyName(
327 std::string_view property_name, std::string_view schema_type) {
328 // Require a property_name
329 if (property_name.empty()) {
330 return absl_ports::InvalidArgumentError(
331 absl_ports::StrCat("Field 'property_name' for schema '", schema_type,
332 "' cannot be empty."));
333 }
334
335 // Only support alphanumeric values.
336 for (char c : property_name) {
337 if (!std::isalnum(c)) {
338 return absl_ports::InvalidArgumentError(
339 absl_ports::StrCat("Field 'property_name' '", property_name,
340 "' can only contain alphanumeric characters."));
341 }
342 }
343
344 return libtextclassifier3::Status::OK;
345 }
346
ValidateDataType(PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)347 libtextclassifier3::Status SchemaUtil::ValidateDataType(
348 PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
349 std::string_view property_name) {
350 // UNKNOWN is the default enum value and should only be used for backwards
351 // compatibility
352 if (data_type == PropertyConfigProto::DataType::UNKNOWN) {
353 return absl_ports::InvalidArgumentError(absl_ports::StrCat(
354 "Field 'data_type' cannot be UNKNOWN for schema property '",
355 schema_type, ".", property_name, "'"));
356 }
357
358 return libtextclassifier3::Status::OK;
359 }
360
ValidateCardinality(PropertyConfigProto::Cardinality::Code cardinality,std::string_view schema_type,std::string_view property_name)361 libtextclassifier3::Status SchemaUtil::ValidateCardinality(
362 PropertyConfigProto::Cardinality::Code cardinality,
363 std::string_view schema_type, std::string_view property_name) {
364 // UNKNOWN is the default enum value and should only be used for backwards
365 // compatibility
366 if (cardinality == PropertyConfigProto::Cardinality::UNKNOWN) {
367 return absl_ports::InvalidArgumentError(absl_ports::StrCat(
368 "Field 'cardinality' cannot be UNKNOWN for schema property '",
369 schema_type, ".", property_name, "'"));
370 }
371
372 return libtextclassifier3::Status::OK;
373 }
374
ValidateStringIndexingConfig(const StringIndexingConfig & config,PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)375 libtextclassifier3::Status SchemaUtil::ValidateStringIndexingConfig(
376 const StringIndexingConfig& config,
377 PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
378 std::string_view property_name) {
379 if (config.term_match_type() == TermMatchType::UNKNOWN &&
380 config.tokenizer_type() != StringIndexingConfig::TokenizerType::NONE) {
381 // They set a tokenizer type, but no term match type.
382 return absl_ports::InvalidArgumentError(absl_ports::StrCat(
383 "Indexed string property '", schema_type, ".", property_name,
384 "' cannot have a term match type UNKNOWN"));
385 }
386
387 if (config.term_match_type() != TermMatchType::UNKNOWN &&
388 config.tokenizer_type() == StringIndexingConfig::TokenizerType::NONE) {
389 // They set a term match type, but no tokenizer type
390 return absl_ports::InvalidArgumentError(
391 absl_ports::StrCat("Indexed string property '", property_name,
392 "' cannot have a tokenizer type of NONE"));
393 }
394
395 return libtextclassifier3::Status::OK;
396 }
397
BuildTypeConfigMap(const SchemaProto & schema,SchemaUtil::TypeConfigMap * type_config_map)398 void SchemaUtil::BuildTypeConfigMap(
399 const SchemaProto& schema, SchemaUtil::TypeConfigMap* type_config_map) {
400 type_config_map->clear();
401 for (const SchemaTypeConfigProto& type_config : schema.types()) {
402 type_config_map->emplace(type_config.schema_type(), type_config);
403 }
404 }
405
ParsePropertyConfigs(const SchemaTypeConfigProto & type_config)406 SchemaUtil::ParsedPropertyConfigs SchemaUtil::ParsePropertyConfigs(
407 const SchemaTypeConfigProto& type_config) {
408 ParsedPropertyConfigs parsed_property_configs;
409
410 // TODO(cassiewang): consider caching property_config_map for some properties,
411 // e.g. using LRU cache. Or changing schema.proto to use go/protomap.
412 for (const PropertyConfigProto& property_config : type_config.properties()) {
413 parsed_property_configs.property_config_map.emplace(
414 property_config.property_name(), &property_config);
415 if (property_config.cardinality() ==
416 PropertyConfigProto::Cardinality::REQUIRED) {
417 parsed_property_configs.num_required_properties++;
418 }
419
420 // A non-default term_match_type indicates that this property is meant to be
421 // indexed.
422 if (property_config.string_indexing_config().term_match_type() !=
423 TermMatchType::UNKNOWN) {
424 parsed_property_configs.num_indexed_properties++;
425 }
426 }
427
428 return parsed_property_configs;
429 }
430
ComputeCompatibilityDelta(const SchemaProto & old_schema,const SchemaProto & new_schema,const DependencyMap & new_schema_dependency_map)431 const SchemaUtil::SchemaDelta SchemaUtil::ComputeCompatibilityDelta(
432 const SchemaProto& old_schema, const SchemaProto& new_schema,
433 const DependencyMap& new_schema_dependency_map) {
434 SchemaDelta schema_delta;
435 schema_delta.index_incompatible = false;
436
437 TypeConfigMap new_type_config_map;
438 BuildTypeConfigMap(new_schema, &new_type_config_map);
439
440 // Iterate through and check each field of the old schema
441 for (const auto& old_type_config : old_schema.types()) {
442 auto new_schema_type_and_config =
443 new_type_config_map.find(old_type_config.schema_type());
444
445 if (new_schema_type_and_config == new_type_config_map.end()) {
446 // Didn't find the old schema type in the new schema, all the old
447 // documents of this schema type are invalid without the schema
448 ICING_VLOG(1) << absl_ports::StrCat("Previously defined schema type '",
449 old_type_config.schema_type(),
450 "' was not defined in new schema");
451 schema_delta.schema_types_deleted.insert(old_type_config.schema_type());
452 continue;
453 }
454
455 ParsedPropertyConfigs new_parsed_property_configs =
456 ParsePropertyConfigs(new_schema_type_and_config->second);
457
458 // We only need to check the old, existing properties to see if they're
459 // compatible since we'll have old data that may be invalidated or need to
460 // be reindexed.
461 int32_t old_required_properties = 0;
462 int32_t old_indexed_properties = 0;
463
464 // If there is a different number of properties, then there must have been a
465 // change.
466 bool is_incompatible = false;
467 bool is_index_incompatible = false;
468 for (const auto& old_property_config : old_type_config.properties()) {
469 if (old_property_config.cardinality() ==
470 PropertyConfigProto::Cardinality::REQUIRED) {
471 ++old_required_properties;
472 }
473
474 // A non-default term_match_type indicates that this property is meant to
475 // be indexed.
476 bool is_indexed_property =
477 old_property_config.string_indexing_config().term_match_type() !=
478 TermMatchType::UNKNOWN;
479 if (is_indexed_property) {
480 ++old_indexed_properties;
481 }
482
483 auto new_property_name_and_config =
484 new_parsed_property_configs.property_config_map.find(
485 old_property_config.property_name());
486
487 if (new_property_name_and_config ==
488 new_parsed_property_configs.property_config_map.end()) {
489 // Didn't find the old property
490 ICING_VLOG(1) << absl_ports::StrCat(
491 "Previously defined property type '", old_type_config.schema_type(),
492 ".", old_property_config.property_name(),
493 "' was not defined in new schema");
494 is_incompatible = true;
495 is_index_incompatible |= is_indexed_property;
496 continue;
497 }
498
499 const PropertyConfigProto* new_property_config =
500 new_property_name_and_config->second;
501
502 if (!IsPropertyCompatible(old_property_config, *new_property_config)) {
503 ICING_VLOG(1) << absl_ports::StrCat(
504 "Property '", old_type_config.schema_type(), ".",
505 old_property_config.property_name(), "' is incompatible.");
506 is_incompatible = true;
507 }
508
509 // Any change in the indexed property requires a reindexing
510 if (!IsTermMatchTypeCompatible(
511 old_property_config.string_indexing_config(),
512 new_property_config->string_indexing_config()) ||
513 old_property_config.document_indexing_config()
514 .index_nested_properties() !=
515 new_property_config->document_indexing_config()
516 .index_nested_properties()) {
517 is_index_incompatible = true;
518 }
519 }
520
521 // We can't have new properties that are REQUIRED since we won't know how
522 // to backfill the data, and the existing data will be invalid. We're
523 // guaranteed from our previous checks that all the old properties are also
524 // present in the new property config, so we can do a simple int comparison
525 // here to detect new required properties.
526 if (new_parsed_property_configs.num_required_properties >
527 old_required_properties) {
528 ICING_VLOG(1) << absl_ports::StrCat(
529 "New schema '", old_type_config.schema_type(),
530 "' has REQUIRED properties that are not "
531 "present in the previously defined schema");
532 is_incompatible = true;
533 }
534
535 // If we've gained any new indexed properties, then the section ids may
536 // change. Since the section ids are stored in the index, we'll need to
537 // reindex everything.
538 if (new_parsed_property_configs.num_indexed_properties >
539 old_indexed_properties) {
540 ICING_VLOG(1) << absl_ports::StrCat(
541 "Set of indexed properties in schema type '",
542 old_type_config.schema_type(),
543 "' has changed, required reindexing.");
544 is_index_incompatible = true;
545 }
546
547 if (is_incompatible) {
548 // If this type is incompatible, then every type that depends on it might
549 // also be incompatible. Use the dependency map to mark those ones as
550 // incompatible too.
551 schema_delta.schema_types_incompatible.insert(
552 old_type_config.schema_type());
553 auto parent_types_itr =
554 new_schema_dependency_map.find(old_type_config.schema_type());
555 if (parent_types_itr != new_schema_dependency_map.end()) {
556 schema_delta.schema_types_incompatible.reserve(
557 schema_delta.schema_types_incompatible.size() +
558 parent_types_itr->second.size());
559 schema_delta.schema_types_incompatible.insert(
560 parent_types_itr->second.begin(), parent_types_itr->second.end());
561 }
562 }
563
564 if (is_index_incompatible) {
565 schema_delta.index_incompatible = true;
566 }
567
568 }
569
570 return schema_delta;
571 }
572
573 } // namespace lib
574 } // namespace icing
575