1ffe3c632Sopenharmony_ci#region Copyright notice and license 2ffe3c632Sopenharmony_ci// Protocol Buffers - Google's data interchange format 3ffe3c632Sopenharmony_ci// Copyright 2015 Google Inc. All rights reserved. 4ffe3c632Sopenharmony_ci// https://developers.google.com/protocol-buffers/ 5ffe3c632Sopenharmony_ci// 6ffe3c632Sopenharmony_ci// Redistribution and use in source and binary forms, with or without 7ffe3c632Sopenharmony_ci// modification, are permitted provided that the following conditions are 8ffe3c632Sopenharmony_ci// met: 9ffe3c632Sopenharmony_ci// 10ffe3c632Sopenharmony_ci// * Redistributions of source code must retain the above copyright 11ffe3c632Sopenharmony_ci// notice, this list of conditions and the following disclaimer. 12ffe3c632Sopenharmony_ci// * Redistributions in binary form must reproduce the above 13ffe3c632Sopenharmony_ci// copyright notice, this list of conditions and the following disclaimer 14ffe3c632Sopenharmony_ci// in the documentation and/or other materials provided with the 15ffe3c632Sopenharmony_ci// distribution. 16ffe3c632Sopenharmony_ci// * Neither the name of Google Inc. nor the names of its 17ffe3c632Sopenharmony_ci// contributors may be used to endorse or promote products derived from 18ffe3c632Sopenharmony_ci// this software without specific prior written permission. 19ffe3c632Sopenharmony_ci// 20ffe3c632Sopenharmony_ci// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21ffe3c632Sopenharmony_ci// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22ffe3c632Sopenharmony_ci// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23ffe3c632Sopenharmony_ci// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24ffe3c632Sopenharmony_ci// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25ffe3c632Sopenharmony_ci// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26ffe3c632Sopenharmony_ci// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27ffe3c632Sopenharmony_ci// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28ffe3c632Sopenharmony_ci// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29ffe3c632Sopenharmony_ci// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30ffe3c632Sopenharmony_ci// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31ffe3c632Sopenharmony_ci#endregion 32ffe3c632Sopenharmony_ci 33ffe3c632Sopenharmony_ciusing System.Collections; 34ffe3c632Sopenharmony_ciusing System.Collections.Generic; 35ffe3c632Sopenharmony_ciusing System.Diagnostics; 36ffe3c632Sopenharmony_ciusing Google.Protobuf.Reflection; 37ffe3c632Sopenharmony_ciusing Google.Protobuf.WellKnownTypes; 38ffe3c632Sopenharmony_ci 39ffe3c632Sopenharmony_cinamespace Google.Protobuf 40ffe3c632Sopenharmony_ci{ 41ffe3c632Sopenharmony_ci /// <summary> 42ffe3c632Sopenharmony_ci /// <para>A tree representation of a FieldMask. Each leaf node in this tree represent 43ffe3c632Sopenharmony_ci /// a field path in the FieldMask.</para> 44ffe3c632Sopenharmony_ci /// 45ffe3c632Sopenharmony_ci /// <para>For example, FieldMask "foo.bar,foo.baz,bar.baz" as a tree will be:</para> 46ffe3c632Sopenharmony_ci /// <code> 47ffe3c632Sopenharmony_ci /// [root] -+- foo -+- bar 48ffe3c632Sopenharmony_ci /// | | 49ffe3c632Sopenharmony_ci /// | +- baz 50ffe3c632Sopenharmony_ci /// | 51ffe3c632Sopenharmony_ci /// +- bar --- baz 52ffe3c632Sopenharmony_ci /// </code> 53ffe3c632Sopenharmony_ci /// 54ffe3c632Sopenharmony_ci /// <para>By representing FieldMasks with this tree structure we can easily convert 55ffe3c632Sopenharmony_ci /// a FieldMask to a canonical form, merge two FieldMasks, calculate the 56ffe3c632Sopenharmony_ci /// intersection to two FieldMasks and traverse all fields specified by the 57ffe3c632Sopenharmony_ci /// FieldMask in a message tree.</para> 58ffe3c632Sopenharmony_ci /// </summary> 59ffe3c632Sopenharmony_ci internal sealed class FieldMaskTree 60ffe3c632Sopenharmony_ci { 61ffe3c632Sopenharmony_ci private const char FIELD_PATH_SEPARATOR = '.'; 62ffe3c632Sopenharmony_ci 63ffe3c632Sopenharmony_ci internal sealed class Node 64ffe3c632Sopenharmony_ci { 65ffe3c632Sopenharmony_ci public Dictionary<string, Node> Children { get; } = new Dictionary<string, Node>(); 66ffe3c632Sopenharmony_ci } 67ffe3c632Sopenharmony_ci 68ffe3c632Sopenharmony_ci private readonly Node root = new Node(); 69ffe3c632Sopenharmony_ci 70ffe3c632Sopenharmony_ci /// <summary> 71ffe3c632Sopenharmony_ci /// Creates an empty FieldMaskTree. 72ffe3c632Sopenharmony_ci /// </summary> 73ffe3c632Sopenharmony_ci public FieldMaskTree() 74ffe3c632Sopenharmony_ci { 75ffe3c632Sopenharmony_ci } 76ffe3c632Sopenharmony_ci 77ffe3c632Sopenharmony_ci /// <summary> 78ffe3c632Sopenharmony_ci /// Creates a FieldMaskTree for a given FieldMask. 79ffe3c632Sopenharmony_ci /// </summary> 80ffe3c632Sopenharmony_ci public FieldMaskTree(FieldMask mask) 81ffe3c632Sopenharmony_ci { 82ffe3c632Sopenharmony_ci MergeFromFieldMask(mask); 83ffe3c632Sopenharmony_ci } 84ffe3c632Sopenharmony_ci 85ffe3c632Sopenharmony_ci public override string ToString() 86ffe3c632Sopenharmony_ci { 87ffe3c632Sopenharmony_ci return ToFieldMask().ToString(); 88ffe3c632Sopenharmony_ci } 89ffe3c632Sopenharmony_ci 90ffe3c632Sopenharmony_ci /// <summary> 91ffe3c632Sopenharmony_ci /// Adds a field path to the tree. In a FieldMask, every field path matches the 92ffe3c632Sopenharmony_ci /// specified field as well as all its sub-fields. For example, a field path 93ffe3c632Sopenharmony_ci /// "foo.bar" matches field "foo.bar" and also "foo.bar.baz", etc. When adding 94ffe3c632Sopenharmony_ci /// a field path to the tree, redundant sub-paths will be removed. That is, 95ffe3c632Sopenharmony_ci /// after adding "foo.bar" to the tree, "foo.bar.baz" will be removed if it 96ffe3c632Sopenharmony_ci /// exists, which will turn the tree node for "foo.bar" to a leaf node. 97ffe3c632Sopenharmony_ci /// Likewise, if the field path to add is a sub-path of an existing leaf node, 98ffe3c632Sopenharmony_ci /// nothing will be changed in the tree. 99ffe3c632Sopenharmony_ci /// </summary> 100ffe3c632Sopenharmony_ci public FieldMaskTree AddFieldPath(string path) 101ffe3c632Sopenharmony_ci { 102ffe3c632Sopenharmony_ci var parts = path.Split(FIELD_PATH_SEPARATOR); 103ffe3c632Sopenharmony_ci if (parts.Length == 0) 104ffe3c632Sopenharmony_ci { 105ffe3c632Sopenharmony_ci return this; 106ffe3c632Sopenharmony_ci } 107ffe3c632Sopenharmony_ci 108ffe3c632Sopenharmony_ci var node = root; 109ffe3c632Sopenharmony_ci var createNewBranch = false; 110ffe3c632Sopenharmony_ci 111ffe3c632Sopenharmony_ci // Find the matching node in the tree. 112ffe3c632Sopenharmony_ci foreach (var part in parts) 113ffe3c632Sopenharmony_ci { 114ffe3c632Sopenharmony_ci // Check whether the path matches an existing leaf node. 115ffe3c632Sopenharmony_ci if (!createNewBranch 116ffe3c632Sopenharmony_ci && node != root 117ffe3c632Sopenharmony_ci && node.Children.Count == 0) 118ffe3c632Sopenharmony_ci { 119ffe3c632Sopenharmony_ci // The path to add is a sub-path of an existing leaf node. 120ffe3c632Sopenharmony_ci return this; 121ffe3c632Sopenharmony_ci } 122ffe3c632Sopenharmony_ci 123ffe3c632Sopenharmony_ci Node childNode; 124ffe3c632Sopenharmony_ci if (!node.Children.TryGetValue(part, out childNode)) 125ffe3c632Sopenharmony_ci { 126ffe3c632Sopenharmony_ci createNewBranch = true; 127ffe3c632Sopenharmony_ci childNode = new Node(); 128ffe3c632Sopenharmony_ci node.Children.Add(part, childNode); 129ffe3c632Sopenharmony_ci } 130ffe3c632Sopenharmony_ci node = childNode; 131ffe3c632Sopenharmony_ci } 132ffe3c632Sopenharmony_ci 133ffe3c632Sopenharmony_ci // Turn the matching node into a leaf node (i.e., remove sub-paths). 134ffe3c632Sopenharmony_ci node.Children.Clear(); 135ffe3c632Sopenharmony_ci return this; 136ffe3c632Sopenharmony_ci } 137ffe3c632Sopenharmony_ci 138ffe3c632Sopenharmony_ci /// <summary> 139ffe3c632Sopenharmony_ci /// Merges all field paths in a FieldMask into this tree. 140ffe3c632Sopenharmony_ci /// </summary> 141ffe3c632Sopenharmony_ci public FieldMaskTree MergeFromFieldMask(FieldMask mask) 142ffe3c632Sopenharmony_ci { 143ffe3c632Sopenharmony_ci foreach (var path in mask.Paths) 144ffe3c632Sopenharmony_ci { 145ffe3c632Sopenharmony_ci AddFieldPath(path); 146ffe3c632Sopenharmony_ci } 147ffe3c632Sopenharmony_ci 148ffe3c632Sopenharmony_ci return this; 149ffe3c632Sopenharmony_ci } 150ffe3c632Sopenharmony_ci 151ffe3c632Sopenharmony_ci /// <summary> 152ffe3c632Sopenharmony_ci /// Converts this tree to a FieldMask. 153ffe3c632Sopenharmony_ci /// </summary> 154ffe3c632Sopenharmony_ci public FieldMask ToFieldMask() 155ffe3c632Sopenharmony_ci { 156ffe3c632Sopenharmony_ci var mask = new FieldMask(); 157ffe3c632Sopenharmony_ci if (root.Children.Count != 0) 158ffe3c632Sopenharmony_ci { 159ffe3c632Sopenharmony_ci var paths = new List<string>(); 160ffe3c632Sopenharmony_ci GetFieldPaths(root, "", paths); 161ffe3c632Sopenharmony_ci mask.Paths.AddRange(paths); 162ffe3c632Sopenharmony_ci } 163ffe3c632Sopenharmony_ci 164ffe3c632Sopenharmony_ci return mask; 165ffe3c632Sopenharmony_ci } 166ffe3c632Sopenharmony_ci 167ffe3c632Sopenharmony_ci /// <summary> 168ffe3c632Sopenharmony_ci /// Gathers all field paths in a sub-tree. 169ffe3c632Sopenharmony_ci /// </summary> 170ffe3c632Sopenharmony_ci private void GetFieldPaths(Node node, string path, List<string> paths) 171ffe3c632Sopenharmony_ci { 172ffe3c632Sopenharmony_ci if (node.Children.Count == 0) 173ffe3c632Sopenharmony_ci { 174ffe3c632Sopenharmony_ci paths.Add(path); 175ffe3c632Sopenharmony_ci return; 176ffe3c632Sopenharmony_ci } 177ffe3c632Sopenharmony_ci 178ffe3c632Sopenharmony_ci foreach (var entry in node.Children) 179ffe3c632Sopenharmony_ci { 180ffe3c632Sopenharmony_ci var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key; 181ffe3c632Sopenharmony_ci GetFieldPaths(entry.Value, childPath, paths); 182ffe3c632Sopenharmony_ci } 183ffe3c632Sopenharmony_ci } 184ffe3c632Sopenharmony_ci 185ffe3c632Sopenharmony_ci /// <summary> 186ffe3c632Sopenharmony_ci /// Adds the intersection of this tree with the given <paramref name="path"/> to <paramref name="output"/>. 187ffe3c632Sopenharmony_ci /// </summary> 188ffe3c632Sopenharmony_ci public void IntersectFieldPath(string path, FieldMaskTree output) 189ffe3c632Sopenharmony_ci { 190ffe3c632Sopenharmony_ci if (root.Children.Count == 0) 191ffe3c632Sopenharmony_ci { 192ffe3c632Sopenharmony_ci return; 193ffe3c632Sopenharmony_ci } 194ffe3c632Sopenharmony_ci 195ffe3c632Sopenharmony_ci var parts = path.Split(FIELD_PATH_SEPARATOR); 196ffe3c632Sopenharmony_ci if (parts.Length == 0) 197ffe3c632Sopenharmony_ci { 198ffe3c632Sopenharmony_ci return; 199ffe3c632Sopenharmony_ci } 200ffe3c632Sopenharmony_ci 201ffe3c632Sopenharmony_ci var node = root; 202ffe3c632Sopenharmony_ci foreach (var part in parts) 203ffe3c632Sopenharmony_ci { 204ffe3c632Sopenharmony_ci if (node != root 205ffe3c632Sopenharmony_ci && node.Children.Count == 0) 206ffe3c632Sopenharmony_ci { 207ffe3c632Sopenharmony_ci // The given path is a sub-path of an existing leaf node in the tree. 208ffe3c632Sopenharmony_ci output.AddFieldPath(path); 209ffe3c632Sopenharmony_ci return; 210ffe3c632Sopenharmony_ci } 211ffe3c632Sopenharmony_ci 212ffe3c632Sopenharmony_ci if (!node.Children.TryGetValue(part, out node)) 213ffe3c632Sopenharmony_ci { 214ffe3c632Sopenharmony_ci return; 215ffe3c632Sopenharmony_ci } 216ffe3c632Sopenharmony_ci } 217ffe3c632Sopenharmony_ci 218ffe3c632Sopenharmony_ci // We found a matching node for the path. All leaf children of this matching 219ffe3c632Sopenharmony_ci // node is in the intersection. 220ffe3c632Sopenharmony_ci var paths = new List<string>(); 221ffe3c632Sopenharmony_ci GetFieldPaths(node, path, paths); 222ffe3c632Sopenharmony_ci foreach (var value in paths) 223ffe3c632Sopenharmony_ci { 224ffe3c632Sopenharmony_ci output.AddFieldPath(value); 225ffe3c632Sopenharmony_ci } 226ffe3c632Sopenharmony_ci } 227ffe3c632Sopenharmony_ci 228ffe3c632Sopenharmony_ci /// <summary> 229ffe3c632Sopenharmony_ci /// Merges all fields specified by this FieldMaskTree from <paramref name="source"/> to <paramref name="destination"/>. 230ffe3c632Sopenharmony_ci /// </summary> 231ffe3c632Sopenharmony_ci public void Merge(IMessage source, IMessage destination, FieldMask.MergeOptions options) 232ffe3c632Sopenharmony_ci { 233ffe3c632Sopenharmony_ci if (source.Descriptor != destination.Descriptor) 234ffe3c632Sopenharmony_ci { 235ffe3c632Sopenharmony_ci throw new InvalidProtocolBufferException("Cannot merge messages of different types."); 236ffe3c632Sopenharmony_ci } 237ffe3c632Sopenharmony_ci 238ffe3c632Sopenharmony_ci if (root.Children.Count == 0) 239ffe3c632Sopenharmony_ci { 240ffe3c632Sopenharmony_ci return; 241ffe3c632Sopenharmony_ci } 242ffe3c632Sopenharmony_ci 243ffe3c632Sopenharmony_ci Merge(root, "", source, destination, options); 244ffe3c632Sopenharmony_ci } 245ffe3c632Sopenharmony_ci 246ffe3c632Sopenharmony_ci /// <summary> 247ffe3c632Sopenharmony_ci /// Merges all fields specified by a sub-tree from <paramref name="source"/> to <paramref name="destination"/>. 248ffe3c632Sopenharmony_ci /// </summary> 249ffe3c632Sopenharmony_ci private void Merge( 250ffe3c632Sopenharmony_ci Node node, 251ffe3c632Sopenharmony_ci string path, 252ffe3c632Sopenharmony_ci IMessage source, 253ffe3c632Sopenharmony_ci IMessage destination, 254ffe3c632Sopenharmony_ci FieldMask.MergeOptions options) 255ffe3c632Sopenharmony_ci { 256ffe3c632Sopenharmony_ci if (source.Descriptor != destination.Descriptor) 257ffe3c632Sopenharmony_ci { 258ffe3c632Sopenharmony_ci throw new InvalidProtocolBufferException($"source ({source.Descriptor}) and destination ({destination.Descriptor}) descriptor must be equal"); 259ffe3c632Sopenharmony_ci } 260ffe3c632Sopenharmony_ci 261ffe3c632Sopenharmony_ci var descriptor = source.Descriptor; 262ffe3c632Sopenharmony_ci foreach (var entry in node.Children) 263ffe3c632Sopenharmony_ci { 264ffe3c632Sopenharmony_ci var field = descriptor.FindFieldByName(entry.Key); 265ffe3c632Sopenharmony_ci if (field == null) 266ffe3c632Sopenharmony_ci { 267ffe3c632Sopenharmony_ci Debug.WriteLine($"Cannot find field \"{entry.Key}\" in message type \"{descriptor.FullName}\""); 268ffe3c632Sopenharmony_ci continue; 269ffe3c632Sopenharmony_ci } 270ffe3c632Sopenharmony_ci 271ffe3c632Sopenharmony_ci if (entry.Value.Children.Count != 0) 272ffe3c632Sopenharmony_ci { 273ffe3c632Sopenharmony_ci if (field.IsRepeated 274ffe3c632Sopenharmony_ci || field.FieldType != FieldType.Message) 275ffe3c632Sopenharmony_ci { 276ffe3c632Sopenharmony_ci Debug.WriteLine($"Field \"{field.FullName}\" is not a singular message field and cannot have sub-fields."); 277ffe3c632Sopenharmony_ci continue; 278ffe3c632Sopenharmony_ci } 279ffe3c632Sopenharmony_ci 280ffe3c632Sopenharmony_ci var sourceField = field.Accessor.GetValue(source); 281ffe3c632Sopenharmony_ci var destinationField = field.Accessor.GetValue(destination); 282ffe3c632Sopenharmony_ci if (sourceField == null 283ffe3c632Sopenharmony_ci && destinationField == null) 284ffe3c632Sopenharmony_ci { 285ffe3c632Sopenharmony_ci // If the message field is not present in both source and destination, skip recursing 286ffe3c632Sopenharmony_ci // so we don't create unnecessary empty messages. 287ffe3c632Sopenharmony_ci continue; 288ffe3c632Sopenharmony_ci } 289ffe3c632Sopenharmony_ci 290ffe3c632Sopenharmony_ci if (destinationField == null) 291ffe3c632Sopenharmony_ci { 292ffe3c632Sopenharmony_ci // If we have to merge but the destination does not contain the field, create it. 293ffe3c632Sopenharmony_ci destinationField = field.MessageType.Parser.CreateTemplate(); 294ffe3c632Sopenharmony_ci field.Accessor.SetValue(destination, destinationField); 295ffe3c632Sopenharmony_ci } 296ffe3c632Sopenharmony_ci 297ffe3c632Sopenharmony_ci var childPath = path.Length == 0 ? entry.Key : path + "." + entry.Key; 298ffe3c632Sopenharmony_ci Merge(entry.Value, childPath, (IMessage)sourceField, (IMessage)destinationField, options); 299ffe3c632Sopenharmony_ci continue; 300ffe3c632Sopenharmony_ci } 301ffe3c632Sopenharmony_ci 302ffe3c632Sopenharmony_ci if (field.IsRepeated) 303ffe3c632Sopenharmony_ci { 304ffe3c632Sopenharmony_ci if (options.ReplaceRepeatedFields) 305ffe3c632Sopenharmony_ci { 306ffe3c632Sopenharmony_ci field.Accessor.Clear(destination); 307ffe3c632Sopenharmony_ci } 308ffe3c632Sopenharmony_ci 309ffe3c632Sopenharmony_ci var sourceField = (IList)field.Accessor.GetValue(source); 310ffe3c632Sopenharmony_ci var destinationField = (IList)field.Accessor.GetValue(destination); 311ffe3c632Sopenharmony_ci foreach (var element in sourceField) 312ffe3c632Sopenharmony_ci { 313ffe3c632Sopenharmony_ci destinationField.Add(element); 314ffe3c632Sopenharmony_ci } 315ffe3c632Sopenharmony_ci } 316ffe3c632Sopenharmony_ci else 317ffe3c632Sopenharmony_ci { 318ffe3c632Sopenharmony_ci var sourceField = field.Accessor.GetValue(source); 319ffe3c632Sopenharmony_ci if (field.FieldType == FieldType.Message) 320ffe3c632Sopenharmony_ci { 321ffe3c632Sopenharmony_ci if (options.ReplaceMessageFields) 322ffe3c632Sopenharmony_ci { 323ffe3c632Sopenharmony_ci if (sourceField == null) 324ffe3c632Sopenharmony_ci { 325ffe3c632Sopenharmony_ci field.Accessor.Clear(destination); 326ffe3c632Sopenharmony_ci } 327ffe3c632Sopenharmony_ci else 328ffe3c632Sopenharmony_ci { 329ffe3c632Sopenharmony_ci field.Accessor.SetValue(destination, sourceField); 330ffe3c632Sopenharmony_ci } 331ffe3c632Sopenharmony_ci } 332ffe3c632Sopenharmony_ci else 333ffe3c632Sopenharmony_ci { 334ffe3c632Sopenharmony_ci if (sourceField != null) 335ffe3c632Sopenharmony_ci { 336ffe3c632Sopenharmony_ci var sourceByteString = ((IMessage)sourceField).ToByteString(); 337ffe3c632Sopenharmony_ci var destinationValue = (IMessage)field.Accessor.GetValue(destination); 338ffe3c632Sopenharmony_ci if (destinationValue != null) 339ffe3c632Sopenharmony_ci { 340ffe3c632Sopenharmony_ci destinationValue.MergeFrom(sourceByteString); 341ffe3c632Sopenharmony_ci } 342ffe3c632Sopenharmony_ci else 343ffe3c632Sopenharmony_ci { 344ffe3c632Sopenharmony_ci field.Accessor.SetValue(destination, field.MessageType.Parser.ParseFrom(sourceByteString)); 345ffe3c632Sopenharmony_ci } 346ffe3c632Sopenharmony_ci } 347ffe3c632Sopenharmony_ci } 348ffe3c632Sopenharmony_ci } 349ffe3c632Sopenharmony_ci else 350ffe3c632Sopenharmony_ci { 351ffe3c632Sopenharmony_ci if (sourceField != null 352ffe3c632Sopenharmony_ci || !options.ReplacePrimitiveFields) 353ffe3c632Sopenharmony_ci { 354ffe3c632Sopenharmony_ci field.Accessor.SetValue(destination, sourceField); 355ffe3c632Sopenharmony_ci } 356ffe3c632Sopenharmony_ci else 357ffe3c632Sopenharmony_ci { 358ffe3c632Sopenharmony_ci field.Accessor.Clear(destination); 359ffe3c632Sopenharmony_ci } 360ffe3c632Sopenharmony_ci } 361ffe3c632Sopenharmony_ci } 362ffe3c632Sopenharmony_ci } 363ffe3c632Sopenharmony_ci } 364ffe3c632Sopenharmony_ci } 365ffe3c632Sopenharmony_ci} 366