tizen 2.3 release
[framework/web/wearable/wrt-security.git] / ace / engine / parser.cpp
1 /*
2  * Copyright (c) 2011 Samsung Electronics Co., Ltd All Rights Reserved
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 #include <memory>
17 #include <functional>
18 #include <string.h>
19 #include <stdarg.h>
20 #include <dpl/log/log.h>
21
22 #include <ace/parser.h>
23 #include <string.h>
24
25 namespace {
26
27 class ParserWarningLogger
28 {
29   public:
30     void operator()(const std::string& logMsg)
31     {
32         LogWarning(logMsg);
33     }
34 };
35
36 class ParserErrorLogger
37 {
38   public:
39     void operator()(const std::string& logMsg)
40     {
41         LogError(logMsg);
42     }
43 };
44
45 template <class Logger>
46 void xmlLogFunction(void* /*ctx*/, const char *msg, ...)
47 {
48     const int BUFFER_SIZE = 1024;
49     char buffer[BUFFER_SIZE];
50     buffer[BUFFER_SIZE - 1] = '\0';
51     Logger l;
52
53     va_list va;
54     va_start(va, msg);
55     vsnprintf(buffer, BUFFER_SIZE - 1, msg, va);
56     va_end(va);
57
58     std::string logmsg(buffer);
59     l(logmsg);
60 }
61
62 }
63
64 const char *Parser::TOKEN_PARAM = "param:";
65
66 Parser::Parser() :
67     ruleId(0),
68     reader(NULL),
69     root(NULL),
70     currentRoot(NULL),
71     currentSubject(NULL),
72     currentCondition(NULL),
73     currentAttribute(NULL),
74     currentText(NULL),
75     processingSignature(false),
76     canonicalizeOnce(false)
77 {
78     processingSignature = true;
79     canonicalizeOnce = true;
80 }
81
82 Parser::~Parser()
83 {
84     /* parse function destroys reader */
85     //  free(this->xmlFilename);
86 }
87
88 TreeNode* Parser::parse(const std::string& filename, const std::string& schema)
89 {
90     if(root != NULL) {
91         root->releaseResources();
92         root = NULL;
93     }
94
95     LogDebug("Parser: opening file " << filename);
96
97     xmlDocPtr xmlDocument = xmlParseFile(filename.c_str());
98     if (!xmlDocument) {
99         LogError("Couldn't parse file " << filename);
100         return root;
101     }
102
103     std::unique_ptr <xmlDoc, std::function<void(xmlDoc*)> >
104         doc(xmlDocument, xmlFreeDoc);
105
106     xmlSchemaParserCtxtPtr xmlSchemaParserContext =
107         xmlSchemaNewParserCtxt(schema.c_str());
108
109     if (!xmlSchemaParserContext) {
110         LogError("Couldn't load xml schema: " << schema);
111         return root;
112     }
113
114     std::unique_ptr <
115                      xmlSchemaParserCtxt,
116                      std::function<void(xmlSchemaParserCtxt*)> >
117                      schemaContext(
118                                    xmlSchemaParserContext,
119                                    xmlSchemaFreeParserCtxt);
120
121     LogDebug("Setting callbacks");
122
123     xmlSchemaSetParserErrors(
124         schemaContext.get(),
125         static_cast<xmlValidityErrorFunc>
126             (&xmlLogFunction<ParserErrorLogger>),
127         static_cast<xmlValidityWarningFunc>
128             (&xmlLogFunction<ParserWarningLogger>),
129         NULL);
130
131     xmlSchemaPtr xmlSchema = xmlSchemaParse(schemaContext.get());
132
133     if (!xmlSchema) {
134         LogError("Couldn't parse xml schema: " << xmlSchema);
135         return root;
136     }
137
138     xmlSchemaValidCtxtPtr xmlValidContext = xmlSchemaNewValidCtxt(xmlSchema);
139
140     if (!xmlValidContext) {
141         LogError("Couldn't create validation context!");
142         xmlSchemaFree(xmlSchema);
143         return root;
144     }
145
146     std::unique_ptr <
147                      xmlSchemaValidCtxt,
148                      std::function<void(xmlSchemaValidCtxt*)> >
149                      schemaValidContext(
150                                         xmlValidContext,
151                                         xmlSchemaFreeValidCtxt);
152
153     xmlSchemaSetValidErrors(
154         schemaValidContext.get(),
155         static_cast<xmlValidityErrorFunc>
156             (&xmlLogFunction<ParserErrorLogger>),
157         static_cast<xmlValidityWarningFunc>
158             (&xmlLogFunction<ParserWarningLogger>),
159         NULL);
160
161     xmlSchemaSetValidOptions(
162                              schemaValidContext.get(),
163                              XML_SCHEMA_VAL_VC_I_CREATE);
164
165     bool result =
166         (xmlSchemaValidateDoc(
167                               schemaValidContext.get(),
168                               xmlDocument) == 0 ? true : false);
169
170     xmlSchemaFree(xmlSchema);
171
172     if (!result) {
173         LogError("Couldn't validate policy file: " << filename <<
174                  " against xml schema: " << schema);
175
176         return root;
177     }
178
179     LogInfo("Policy file: " << filename << " validated!");
180
181     xmlTextReaderPtr xmlReader = xmlReaderWalker(xmlDocument);
182
183     //[CR] consider using ASSERT/DASSERT
184     if (NULL == xmlReader) {
185         LogError("Error, xml reader cannot be created. Probably xml file is missing (opening file " << filename << ")");
186         return root;
187     }
188
189     std::unique_ptr <xmlTextReader, std::function<void(xmlTextReader*)> >
190          reader(xmlReader, xmlFreeTextReader);
191
192     int ret;
193     ret = xmlTextReaderRead(reader.get());
194     while (ret == 1) {
195         std::unique_ptr<xmlChar, std::function<void(xmlChar*)> >
196             name(xmlTextReaderName(reader.get()), xmlFree);
197
198         if (!strcmp("policy-set", (const char *)name.get())) {
199             processingSignature = false;
200         } else if (!strcmp("SignedInfo",
201                            (const char *)name.get()) && canonicalizeOnce) {
202             #if 0 //TODO I think we don't need canonicalization in ACE only in PM,
203             //we have to  verify it tough
204             extractNodeToFile(reader, "output.xml");
205             //TODO we should be able to handle more than one canonicalization algorithm
206             canonicalize("output.xml", "canon.xml", Canonicalization::C14N);
207             canonicalizeOnce = false;
208             #endif
209         }
210         //Do not process signature of xml file
211         if(!processingSignature) {
212             processNode(reader.get());
213         }
214         ret = xmlTextReaderRead(reader.get());
215     }
216
217     if (ret != 0) {
218         LogError("Error while parsing XML file");
219         if (root) {
220             root->releaseResources();
221             root = NULL;
222         }
223     }
224
225     return root;
226 }
227
228 void Parser::processNode(xmlTextReaderPtr reader)
229 {
230     //TODO this is interesting, xmlTextReaderNodeType returns int but I am pretty sure
231     //those integers coresponds to xmlReaderTypes
232     xmlReaderTypes type =
233         static_cast<xmlReaderTypes>(xmlTextReaderNodeType(reader));
234
235     switch (type) {
236     //Start element
237     case XML_READER_TYPE_ELEMENT:
238         startNodeHandler(reader);
239         break;
240     //End element
241     case XML_READER_TYPE_END_ELEMENT:
242         endNodeHandler(reader);
243         break;
244     //Text element
245     case XML_READER_TYPE_TEXT:
246         textNodeHandler(reader);
247         break;
248     default:
249         //Do not handle other xml tags
250         break;
251     }
252 }
253
254 void Parser::startNodeHandler(xmlTextReaderPtr reader)
255 {
256     xmlChar *name = xmlTextReaderName(reader);
257
258     switch (*name) {
259     case 'p':     //policy and policy-set
260         if (*(name + 6) == 0) {
261             handlePolicy(reader, TreeNode::Policy);
262         } else {
263             handlePolicy(reader, TreeNode::PolicySet);
264         }
265         break;
266     case 'r':     //rule and resource-match
267         if (*(name + 1) == 'u') {
268             handleRule(reader);
269         } else if (*(name + 9) == 'm') {
270             handleMatch(reader, Attribute::Type::Resource);
271         } else {
272             handleAttr(reader);
273         }
274         break;
275     case 's':     //subject and subject-match
276         if (*(name + 7) == 0) {
277             handleSubject();
278         } else if (*(name + 8) == 'm') { //subject match
279             handleSubjectMatch(reader);
280         } else {  //subject attr
281             handleAttr(reader);
282         }
283         break;
284     case 'c':    //condition
285         handleCondition(reader);
286         break;
287     case 'e':    //environment-match
288         if (*(name + 12) == 'm') {
289             handleMatch(reader, Attribute::Type::Environment);
290         } else {  //env-attr
291             handleAttr(reader);
292         }
293         break;
294     }
295     xmlFree(name);
296 }
297
298 void Parser::endNodeHandler(xmlTextReaderPtr reader)
299 {
300     xmlChar *name = xmlTextReaderName(reader);
301
302     switch (*name) {
303     case 'p':     //policy and policy-set
304         //Restore old root
305         currentRoot = currentRoot->getParent();
306         break;
307     case 'r':     //Rule and resource match
308         if (*(name + 1) == 'u') { //Rule
309             currentRoot = currentRoot->getParent();
310         } else {  //Resource-match
311             consumeCurrentText();     //consume text if any available
312             consumeCurrentAttribute();     //consume attribute
313         }
314         break;
315     case 's':     //subject and subject-match
316         if (*(name + 7) == 0) { //handle subject
317             consumeCurrentSubject();
318         } else if (*(name + 8) == 'm') { //handle subject match
319             consumeCurrentText();
320             consumeSubjectMatch();
321         }
322         //Subject-match end doesn't require handling
323         break;
324     case 'c':    //condition
325         consumeCurrentCondition();
326         break;
327     case 'e':    //environment-match
328         consumeCurrentText();     //consume text if any available
329         consumeCurrentAttribute();     //consume attribute
330         break;
331     }
332     xmlFree(name);
333 }
334
335 void Parser::textNodeHandler(xmlTextReaderPtr reader)
336 {
337     delete currentText;
338     xmlChar * text = xmlTextReaderValue(reader);
339     Assert(text != NULL && "Parser couldn't parse PCDATA");
340
341     currentText = new std::string(reinterpret_cast<const char * >(text));
342     trim(currentText);
343     xmlFree(text);
344 }
345
346 void Parser::handlePolicy(xmlTextReaderPtr reader,
347         TreeNode::TypeID type)
348 {
349     Policy::CombineAlgorithm algorithm;
350
351     //Get first attribute
352     xmlChar * combAlg = xmlTextReaderGetAttribute(reader, BAD_CAST("combine"));
353
354     Assert(combAlg != NULL && "Parser error while getting attributes");
355     algorithm = convertToCombineAlgorithm(combAlg);
356
357     //Create TreeNode element
358     Policy * policy = NULL;
359     if (type == TreeNode::Policy) {
360         policy = new Policy();
361     } else {
362         policy = new PolicySet();
363     }
364     policy->setCombineAlgorithm(algorithm);
365     TreeNode * node = new TreeNode(currentRoot, type, policy);
366     //Add new tree node to current's root children set
367     if (currentRoot != NULL) {
368         currentRoot->addChild(node);
369     }
370
371     //Switch the current root to the new node
372     if (!xmlTextReaderIsEmptyElement(reader)) {
373         //Current root switching is necessary only if tag is not empty
374         currentRoot = node;
375     }
376     if (root == NULL) {
377         root = currentRoot;
378     }
379
380     if (NULL == currentRoot) {
381         node->releaseResources();
382     }
383
384     xmlFree(combAlg);
385 }
386
387 void Parser::handleRule(xmlTextReaderPtr reader)
388 {
389     ExtendedEffect effect(Inapplicable);
390
391     //[CR] create macros for attribute names
392     xmlChar * eff = xmlTextReaderGetAttribute(reader, BAD_CAST("effect")); //get the rule attribute
393
394     Assert(eff != NULL && "Parser error while getting attributes");
395     effect = convertToEffect(eff);
396
397     Rule * rule = NULL;
398     rule = new Rule();
399     rule->setEffect(effect);
400
401     TreeNode * node = new TreeNode(currentRoot, TreeNode::Rule, rule);
402     //Add new tree node to current's root children set
403     if (currentRoot != NULL) { //
404         currentRoot->addChild(node);
405     }
406
407     if (!xmlTextReaderIsEmptyElement(reader)) {
408         currentRoot = node;
409     }
410
411     if (NULL == currentRoot) {
412         node->releaseResources();
413     }
414
415     xmlFree(eff);
416 }
417
418 void Parser::handleSubject()
419 {
420     currentSubject = new Subject();
421     //TODO what about empty subject tag
422 }
423
424 void Parser::handleCondition(xmlTextReaderPtr reader)
425 {
426     Condition::CombineType combineType = Condition::AND;
427
428     xmlChar * combine = xmlTextReaderGetAttribute(reader, BAD_CAST("combine")); //get the rule attribute
429
430     Assert(combine != NULL && "Parser error while getting attributes");
431
432     combineType = *combine == 'a' ? Condition::AND : Condition::OR;
433
434     Condition * condition = new Condition();
435     condition->setCombineType(combineType);
436     condition->setParent(currentCondition);
437
438     currentCondition = condition;
439     xmlFree(combine);
440     //TODO what about empty condition tag?
441 }
442
443 //Subject match is handled differently than resource or environment match
444 //Because it cannot have any children tags and can only include PCDATA
445 void Parser::handleSubjectMatch(xmlTextReaderPtr reader)
446 {
447     //processing Subject
448     int attributes = xmlTextReaderAttributeCount(reader);
449
450     xmlChar * func = NULL;
451     xmlChar * value = NULL;
452     xmlChar * attrName = xmlTextReaderGetAttribute(reader, BAD_CAST("attr")); //get the first attribute
453
454     if (attributes == 2) {
455         //match attribute ommited, text value will be used
456         func = xmlTextReaderGetAttribute(reader, BAD_CAST("func"));
457     } else if (attributes == 3) {
458         value = xmlTextReaderGetAttribute(reader, BAD_CAST("match"));
459         func = xmlTextReaderGetAttribute(reader, BAD_CAST("func"));
460     } else {
461         Assert(false && "Wrong XML file format");
462     }
463
464     // creating temporiary object is not good idea
465     // but we have no choice untill Attribute have constructor taking std::string*
466     std::string temp(reinterpret_cast<const char *>(attrName));
467     Attribute * attr = new Attribute(&temp, convertToMatchFunction(
468                                          func), Attribute::Type::Subject);
469     if (value != NULL) { //add value of the attribute if possible
470         //[CR] consider create Attribute::addValue(char *) function
471         std::string temp(reinterpret_cast<const char *>(value));
472         attr->addValue(&temp);
473     }
474     currentAttribute = attr;
475
476     if (xmlTextReaderIsEmptyElement(reader)) {
477         Assert(value != NULL && "XML file format is wrong");
478         //Attribute value is required to obtain the match value easier
479         consumeSubjectMatch(value);
480     }
481
482     if (attributes == 2 || attributes == 3) {
483         xmlFree(func);
484     }
485     xmlFree(value);
486     xmlFree(attrName);
487 }
488
489 void Parser::handleMatch(xmlTextReaderPtr reader,
490         Attribute::Type type)
491 {
492     int attributes = xmlTextReaderAttributeCount(reader);
493
494     xmlChar * func = NULL;
495     xmlChar * value = NULL;
496     xmlChar * attrName = xmlTextReaderGetAttribute(reader, BAD_CAST("attr")); //get the first attribute
497
498     if (attributes == 2) {
499         //match attribute ommited, text value will be used
500         func = xmlTextReaderGetAttribute(reader, BAD_CAST("func"));
501         //the content may be resource-attr or PCDATA
502     } else if (attributes == 3) {
503         value = xmlTextReaderGetAttribute(reader, BAD_CAST("match"));
504         func = xmlTextReaderGetAttribute(reader, BAD_CAST("func"));
505     } else {
506         Assert(false && "Wrong XML file format");
507     }
508
509     // FunctionParam type is sybtype of Resource.
510     // FunctionParam is used to storage attriburess of call functions.
511     if (0 ==
512         xmlStrncmp(attrName, BAD_CAST(TOKEN_PARAM),
513                    xmlStrlen(BAD_CAST(TOKEN_PARAM))) && type ==
514         Attribute::Type::Resource) {
515         type = Attribute::Type::FunctionParam;
516     }
517
518     std::string temp(reinterpret_cast<const char*>(attrName));
519     Attribute * attr = new Attribute(&temp, convertToMatchFunction(func), type);
520     currentAttribute = attr;
521
522     if (xmlTextReaderIsEmptyElement(reader)) {
523         Assert(value != NULL && "XML is currupted");
524         std::string tempVal(reinterpret_cast<const char*>(value));
525         currentAttribute->addValue(&tempVal);
526         consumeCurrentAttribute();
527     }
528
529     if (attributes == 2 || attributes == 3) {
530         xmlFree(func);
531     }
532     xmlFree(value);
533     xmlFree(attrName);
534 }
535
536 Policy::CombineAlgorithm Parser::convertToCombineAlgorithm(xmlChar* algorithm)
537 {
538     switch (*algorithm) {
539     case 'f':
540         if (*(algorithm + 6) == 'a') { //first applicable
541             return Policy::FirstApplicable;
542         }
543         return Policy::FirstTargetMatching;
544     case 'd':
545         return Policy::DenyOverride;
546     case 'p':
547         return Policy::PermitOverride;
548     default:
549         Assert(false && "Wrong combine algorithm name");
550         return Policy::DenyOverride;
551     }
552 }
553
554 ExtendedEffect Parser::convertToEffect(xmlChar *effect)
555 {
556     switch (*effect) {
557     case 'd':     //deny
558         return Deny;
559         break;
560     case 'p':
561         //permit, prompt-blanket, prompt-session, prompt-oneshot
562         if (*(effect + 1) == 'e') {
563             return ExtendedEffect(Permit, ruleId++);
564         }
565         switch (*(effect + 7)) {
566         case 'b':
567             return ExtendedEffect(PromptBlanket, ruleId++);
568         case 's':
569             return ExtendedEffect(PromptSession, ruleId++);
570         case 'o':
571             return ExtendedEffect(PromptOneShot, ruleId++);
572         default:
573             Assert(false && "Effect is Error");
574             return ExtendedEffect();
575         }
576         break;
577     default:
578         Assert(false && "Effect is Error");
579         return ExtendedEffect();
580     }
581     //return ExtendedEffect(Inapplicable);
582 }
583
584 Attribute::Match Parser::convertToMatchFunction(xmlChar * func)
585 {
586     if (func == NULL) {
587         LogError("[ERROR] match function value is NULL");
588         return Attribute::Match::Error;
589     }
590
591     if (*func == 'g') {
592         return Attribute::Match::Glob;
593     } else if (*func == 'e') {
594         return Attribute::Match::Equal;
595     } else if (*func == 'r') {
596         return Attribute::Match::Regexp;
597     } else {
598         LogError("[ERROR] match function value is NULL");
599         return Attribute::Match::Error;
600     }
601     //Assert(false);
602 }
603
604 void Parser::handleAttr(xmlTextReaderPtr reader)
605 {
606     xmlChar * attrValue = xmlTextReaderGetAttribute(reader, BAD_CAST("attr")); //get the first attribute
607     Assert(attrValue != NULL && "Error while obtaining attribute");
608
609     std::string temp(reinterpret_cast<const char*>(attrValue));
610     currentAttribute->addValue(&temp);
611
612     xmlFree(attrValue);
613 }
614
615 void Parser::consumeCurrentText()
616 {
617     Assert(currentText != NULL);
618     currentAttribute->addValue(currentText);
619     delete currentText;
620
621     currentText = NULL;
622 }
623
624 void Parser::consumeCurrentAttribute()
625 {
626     Assert(currentAttribute != NULL);
627
628     currentCondition->addAttribute(*currentAttribute);
629     delete currentAttribute;
630
631     currentAttribute = NULL;
632 }
633
634 void Parser::consumeCurrentSubject()
635 {
636     Policy * policy = dynamic_cast<Policy *>(currentRoot->getElement());
637     Assert(policy != NULL);
638     policy->addSubject(currentSubject);
639     //TODO maybe keep subjects not subject pointers in Policies and consume subjects here
640     currentSubject = NULL;
641 }
642
643 void Parser::consumeCurrentCondition()
644 {
645     Condition * temp = NULL;
646     if (currentCondition != NULL) {
647         if (currentCondition->getParent() != NULL) { //Condition is a child of another condition
648             currentCondition->getParent()->addCondition(*currentCondition);
649         } else { //Condition parent is a Rule
650             Rule * rule = dynamic_cast<Rule *>(currentRoot->getElement());
651             Assert(rule != NULL);
652             rule->setCondition(*currentCondition);
653         }
654         temp = currentCondition->getParent();
655         delete currentCondition;
656     }
657     currentCondition = temp;  //switch current condition ( it may be switched to NULL if condition's parent was rule
658 }
659
660 void Parser::consumeSubjectMatch(xmlChar * value)
661 {
662     Assert(
663         currentAttribute != NULL &&
664         "consuming subject match without attribute set");
665
666     if (currentSubject != NULL) {
667         currentSubject->addNewAttribute(*currentAttribute);
668         //[CR] matching/modyfing functions transform uri.host to uri ( etc. ) so strncmp is not needed, string equality will do
669         if (!strncmp(currentAttribute->getName()->c_str(), "uri",
670                      3) ||
671             !strncmp(currentAttribute->getName()->c_str(), "id", 2)) {
672             if (value != NULL) {
673                 currentSubject->setSubjectId(reinterpret_cast<const char *>(
674                                                  value));
675             } else if (currentAttribute->getValue()->size()) {
676                 currentSubject->setSubjectId(
677                     currentAttribute->getValue()->front());
678             } else {
679                 Assert(false);
680             }
681         }
682     } else if (currentCondition != NULL) {
683         currentCondition->addAttribute(*currentAttribute);
684     }
685
686     delete currentAttribute;
687     currentAttribute = NULL;
688 }
689
690 void Parser::trim(std::string * str)
691 {
692     std::string::size_type pos = str->find_last_not_of(whitespaces);
693     if (pos != std::string::npos) {
694         str->erase(pos + 1);
695         pos = str->find_first_not_of(whitespaces);
696         if (pos != std::string::npos) {
697             str->erase(0, pos);
698         }
699     } else {
700         str->erase(str->begin(), str->end());
701         LogInfo("Warning, empty string as attribute value");
702     }
703 }
704
705 // KW void Parser::canonicalize(const char * input, const char * output, CanonicalizationAlgorithm canonicalizationAlgorithm){
706 // KW
707 // KW     xmlDocPtr       doc =  xmlParseFile(input);
708 // KW     //xmlDocDump(stdout, doc);
709 // KW
710 // KW     if(doc == NULL)
711 // KW     {
712 // KW         LogError("Canonicalization error, cannot parser xml file");
713 // KW     }
714 // KW
715 // KW
716 // KW     int mode = -1;
717 // KW     if(canonicalizationAlgorithm == C14N)
718 // KW     {
719 // KW         mode = 0;
720 // KW     }
721 // KW     else if(canonicalizationAlgorithm == C14NEXCLUSIVE)
722 // KW     {
723 // KW         mode = 1;
724 // KW     }
725 // KW
726 // KW
727 // KW     xmlC14NDocSave(doc, NULL, mode, NULL, 0, output, 0);
728 // KW
729 // KW     xmlFreeDoc(doc);
730 // KW
731 // KW }
732
733 // KW int Parser::extractNodeToFile(xmlTextReaderPtr reader, const char * filename){
734 // KW
735 // KW        xmlNodePtr node = xmlTextReaderExpand(reader);
736 // KW        xmlBufferPtr buff = xmlBufferCreate();
737 // KW        xmlNodeDump(buff, node->doc, node, 0, 0);
738 // KW        FILE * file = fopen(filename, "w");
739 // KW        if(file == NULL){
740 // KW            LogError("Error while trying to open file "<<filename);
741 // KW            return -1;
742 // KW        }
743 // KW        int ret = xmlBufferDump(file, buff);
744 // KW        fclose(file);
745 // KW        xmlBufferFree(buff);
746 // KW        return ret;
747 // KW
748 // KW }
749