1 /* ScummVM - Graphic Adventure Engine
2 *
3 * ScummVM is the legal property of its developers, whose names
4 * are too numerous to list here. Please refer to the COPYRIGHT
5 * file distributed with this source distribution.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23 #include "common/scummsys.h"
24 #include "common/debug.h"
25 #include "common/util.h"
26 #include "common/file.h"
27 #include "common/fs.h"
28 #include "common/macresman.h"
29 #include "common/md5.h"
30 #include "common/substream.h"
31 #include "common/textconsole.h"
32 #include "common/archive.h"
33
34 #ifdef MACOSX
35 #include "common/config-manager.h"
36 #endif
37
38 namespace Common {
39
40 #define MBI_INFOHDR 128
41 #define MBI_ZERO1 0
42 #define MBI_NAMELEN 1
43 #define MBI_ZERO2 74
44 #define MBI_ZERO3 82
45 #define MBI_DFLEN 83
46 #define MBI_RFLEN 87
47 #define MAXNAMELEN 63
48
MacResManager()49 MacResManager::MacResManager() {
50 memset(this, 0, sizeof(MacResManager));
51 close();
52 }
53
~MacResManager()54 MacResManager::~MacResManager() {
55 close();
56 }
57
close()58 void MacResManager::close() {
59 _resForkOffset = -1;
60 _mode = kResForkNone;
61
62 for (int i = 0; i < _resMap.numTypes; i++) {
63 for (int j = 0; j < _resTypes[i].items; j++)
64 if (_resLists[i][j].nameOffset != -1)
65 delete[] _resLists[i][j].name;
66
67 delete[] _resLists[i];
68 }
69
70 delete[] _resLists; _resLists = nullptr;
71 delete[] _resTypes; _resTypes = nullptr;
72 delete _stream; _stream = nullptr;
73 _resMap.numTypes = 0;
74 }
75
hasDataFork() const76 bool MacResManager::hasDataFork() const {
77 return !_baseFileName.empty();
78 }
79
hasResFork() const80 bool MacResManager::hasResFork() const {
81 return !_baseFileName.empty() && _mode != kResForkNone;
82 }
83
getResForkDataSize() const84 uint32 MacResManager::getResForkDataSize() const {
85 if (!hasResFork())
86 return 0;
87
88 _stream->seek(_resForkOffset + 4);
89 return _stream->readUint32BE();
90 }
91
computeResForkMD5AsString(uint32 length) const92 String MacResManager::computeResForkMD5AsString(uint32 length) const {
93 if (!hasResFork())
94 return String();
95
96 _stream->seek(_resForkOffset);
97 uint32 dataOffset = _stream->readUint32BE() + _resForkOffset;
98 /* uint32 mapOffset = */ _stream->readUint32BE();
99 uint32 dataLength = _stream->readUint32BE();
100
101
102 SeekableSubReadStream resForkStream(_stream, dataOffset, dataOffset + dataLength);
103 return computeStreamMD5AsString(resForkStream, MIN<uint32>(length, _resForkSize));
104 }
105
open(const String & fileName)106 bool MacResManager::open(const String &fileName) {
107 close();
108
109 #ifdef WIIU
110 //FIXME crash here on wiiU
111 return false;
112 #endif
113
114 #ifdef MACOSX
115 // Check the actual fork on a Mac computer
116 String fullPath = ConfMan.get("path") + "/" + fileName + "/..namedfork/rsrc";
117 FSNode resFsNode = FSNode(fullPath);
118 if (resFsNode.exists()) {
119 SeekableReadStream *macResForkRawStream = resFsNode.createReadStream();
120
121 if (macResForkRawStream && loadFromRawFork(*macResForkRawStream)) {
122 _baseFileName = fileName;
123 return true;
124 }
125
126 delete macResForkRawStream;
127 }
128 #endif
129
130 File *file = new File();
131
132 // Prefer standalone files first, starting with raw forks
133 if (file->open(fileName + ".rsrc") && loadFromRawFork(*file)) {
134 _baseFileName = fileName;
135 return true;
136 }
137 file->close();
138
139 // Then try for AppleDouble using Apple's naming
140 if (file->open(constructAppleDoubleName(fileName)) && loadFromAppleDouble(*file)) {
141 _baseFileName = fileName;
142 return true;
143 }
144 file->close();
145
146 // Check .bin for MacBinary next
147 if (file->open(fileName + ".bin") && loadFromMacBinary(*file)) {
148 _baseFileName = fileName;
149 return true;
150 }
151 file->close();
152
153 // As a last resort, see if just the data fork exists
154 if (file->open(fileName)) {
155 _baseFileName = fileName;
156
157 // FIXME: Is this really needed?
158 if (isMacBinary(*file)) {
159 file->seek(0);
160 if (loadFromMacBinary(*file))
161 return true;
162 }
163
164 file->seek(0);
165 _stream = file;
166 return true;
167 }
168
169 delete file;
170
171 // The file doesn't exist
172 return false;
173 }
174
open(const FSNode & path,const String & fileName)175 bool MacResManager::open(const FSNode &path, const String &fileName) {
176 close();
177
178 #ifdef MACOSX
179 // Check the actual fork on a Mac computer
180 String fullPath = path.getPath() + "/" + fileName + "/..namedfork/rsrc";
181 FSNode resFsNode = FSNode(fullPath);
182 if (resFsNode.exists()) {
183 SeekableReadStream *macResForkRawStream = resFsNode.createReadStream();
184
185 if (macResForkRawStream && loadFromRawFork(*macResForkRawStream)) {
186 _baseFileName = fileName;
187 return true;
188 }
189
190 delete macResForkRawStream;
191 }
192 #endif
193
194 // Prefer standalone files first, starting with raw forks
195 FSNode fsNode = path.getChild(fileName + ".rsrc");
196 if (fsNode.exists() && !fsNode.isDirectory()) {
197 SeekableReadStream *stream = fsNode.createReadStream();
198 if (loadFromRawFork(*stream)) {
199 _baseFileName = fileName;
200 return true;
201 }
202 delete stream;
203 }
204
205 // Then try for AppleDouble using Apple's naming
206 fsNode = path.getChild(constructAppleDoubleName(fileName));
207 if (fsNode.exists() && !fsNode.isDirectory()) {
208 SeekableReadStream *stream = fsNode.createReadStream();
209 if (loadFromAppleDouble(*stream)) {
210 _baseFileName = fileName;
211 return true;
212 }
213 delete stream;
214 }
215
216 // Check .bin for MacBinary next
217 fsNode = path.getChild(fileName + ".bin");
218 if (fsNode.exists() && !fsNode.isDirectory()) {
219 SeekableReadStream *stream = fsNode.createReadStream();
220 if (loadFromMacBinary(*stream)) {
221 _baseFileName = fileName;
222 return true;
223 }
224 delete stream;
225 }
226
227 // As a last resort, see if just the data fork exists
228 fsNode = path.getChild(fileName);
229 if (fsNode.exists() && !fsNode.isDirectory()) {
230 SeekableReadStream *stream = fsNode.createReadStream();
231 _baseFileName = fileName;
232
233 // FIXME: Is this really needed?
234 if (isMacBinary(*stream)) {
235 stream->seek(0);
236 if (loadFromMacBinary(*stream))
237 return true;
238 }
239
240 stream->seek(0);
241 _stream = stream;
242 return true;
243 }
244
245 // The file doesn't exist
246 return false;
247 }
248
exists(const String & fileName)249 bool MacResManager::exists(const String &fileName) {
250 // Try the file name by itself
251 if (File::exists(fileName))
252 return true;
253
254 // Try the .rsrc extension
255 if (File::exists(fileName + ".rsrc"))
256 return true;
257
258 // Check if we have a MacBinary file
259 File tempFile;
260 if (tempFile.open(fileName + ".bin") && isMacBinary(tempFile))
261 return true;
262
263 // Check if we have an AppleDouble file
264 if (tempFile.open(constructAppleDoubleName(fileName)) && tempFile.readUint32BE() == 0x00051607)
265 return true;
266
267 return false;
268 }
269
listFiles(StringArray & files,const String & pattern)270 void MacResManager::listFiles(StringArray &files, const String &pattern) {
271 // Base names discovered so far.
272 typedef HashMap<String, bool, IgnoreCase_Hash, IgnoreCase_EqualTo> BaseNameSet;
273 BaseNameSet baseNames;
274
275 // List files itself.
276 ArchiveMemberList memberList;
277 SearchMan.listMatchingMembers(memberList, pattern);
278 SearchMan.listMatchingMembers(memberList, pattern + ".rsrc");
279 SearchMan.listMatchingMembers(memberList, pattern + ".bin");
280 SearchMan.listMatchingMembers(memberList, constructAppleDoubleName(pattern));
281
282 for (ArchiveMemberList::const_iterator i = memberList.begin(), end = memberList.end(); i != end; ++i) {
283 String filename = (*i)->getName();
284
285 // For raw resource forks and MacBinary files we strip the extension
286 // here to obtain a valid base name.
287 int lastDotPos = filename.size() - 1;
288 for (; lastDotPos >= 0; --lastDotPos) {
289 if (filename[lastDotPos] == '.') {
290 break;
291 }
292 }
293
294 if (lastDotPos != -1) {
295 const char *extension = filename.c_str() + lastDotPos + 1;
296 bool removeExtension = false;
297
298 // TODO: Should we really keep filenames suggesting raw resource
299 // forks or MacBinary files but not being such around? This might
300 // depend on the pattern the client requests...
301 if (!scumm_stricmp(extension, "rsrc")) {
302 SeekableReadStream *stream = (*i)->createReadStream();
303 removeExtension = stream && isRawFork(*stream);
304 delete stream;
305 } else if (!scumm_stricmp(extension, "bin")) {
306 SeekableReadStream *stream = (*i)->createReadStream();
307 removeExtension = stream && isMacBinary(*stream);
308 delete stream;
309 }
310
311 if (removeExtension) {
312 filename.erase(lastDotPos);
313 }
314 }
315
316 // Strip AppleDouble '._' prefix if applicable.
317 bool isAppleDoubleName = false;
318 const String filenameAppleDoubleStripped = disassembleAppleDoubleName(filename, &isAppleDoubleName);
319
320 if (isAppleDoubleName) {
321 SeekableReadStream *stream = (*i)->createReadStream();
322 if (stream->readUint32BE() == 0x00051607) {
323 filename = filenameAppleDoubleStripped;
324 }
325 // TODO: Should we really keep filenames suggesting AppleDouble
326 // but not being AppleDouble around? This might depend on the
327 // pattern the client requests...
328 delete stream;
329 }
330
331 baseNames[filename] = true;
332 }
333
334 // Append resulting base names to list to indicate found files.
335 for (BaseNameSet::const_iterator i = baseNames.begin(), end = baseNames.end(); i != end; ++i) {
336 files.push_back(i->_key);
337 }
338 }
339
loadFromAppleDouble(SeekableReadStream & stream)340 bool MacResManager::loadFromAppleDouble(SeekableReadStream &stream) {
341 if (stream.readUint32BE() != 0x00051607) // tag
342 return false;
343
344 stream.skip(20); // version + home file system
345
346 uint16 entryCount = stream.readUint16BE();
347
348 for (uint16 i = 0; i < entryCount; i++) {
349 uint32 id = stream.readUint32BE();
350 uint32 offset = stream.readUint32BE();
351 uint32 length = stream.readUint32BE(); // length
352
353 if (id == 2) {
354 // Found the resource fork!
355 _resForkOffset = offset;
356 _mode = kResForkAppleDouble;
357 _resForkSize = length;
358 return load(stream);
359 }
360 }
361
362 return false;
363 }
364
isMacBinary(SeekableReadStream & stream)365 bool MacResManager::isMacBinary(SeekableReadStream &stream) {
366 byte infoHeader[MBI_INFOHDR];
367 int resForkOffset = -1;
368
369 stream.read(infoHeader, MBI_INFOHDR);
370
371 if (infoHeader[MBI_ZERO1] == 0 && infoHeader[MBI_ZERO2] == 0 &&
372 infoHeader[MBI_ZERO3] == 0 && infoHeader[MBI_NAMELEN] <= MAXNAMELEN) {
373
374 // Pull out fork lengths
375 uint32 dataSize = READ_BE_UINT32(infoHeader + MBI_DFLEN);
376 uint32 rsrcSize = READ_BE_UINT32(infoHeader + MBI_RFLEN);
377
378 uint32 dataSizePad = (((dataSize + 127) >> 7) << 7);
379 uint32 rsrcSizePad = (((rsrcSize + 127) >> 7) << 7);
380
381 // Length check
382 if (MBI_INFOHDR + dataSizePad + rsrcSizePad == (uint32)stream.size()) {
383 resForkOffset = MBI_INFOHDR + dataSizePad;
384 }
385 }
386
387 if (resForkOffset < 0)
388 return false;
389
390 return true;
391 }
392
isRawFork(SeekableReadStream & stream)393 bool MacResManager::isRawFork(SeekableReadStream &stream) {
394 // TODO: Is there a better way to detect whether this is a raw fork?
395 const uint32 dataOffset = stream.readUint32BE();
396 const uint32 mapOffset = stream.readUint32BE();
397 const uint32 dataLength = stream.readUint32BE();
398 const uint32 mapLength = stream.readUint32BE();
399
400 return !stream.eos() && !stream.err()
401 && dataOffset < (uint32)stream.size() && dataOffset + dataLength <= (uint32)stream.size()
402 && mapOffset < (uint32)stream.size() && mapOffset + mapLength <= (uint32)stream.size();
403 }
404
loadFromMacBinary(SeekableReadStream & stream)405 bool MacResManager::loadFromMacBinary(SeekableReadStream &stream) {
406 byte infoHeader[MBI_INFOHDR];
407 stream.read(infoHeader, MBI_INFOHDR);
408
409 // Maybe we have MacBinary?
410 if (infoHeader[MBI_ZERO1] == 0 && infoHeader[MBI_ZERO2] == 0 &&
411 infoHeader[MBI_ZERO3] == 0 && infoHeader[MBI_NAMELEN] <= MAXNAMELEN) {
412
413 // Pull out fork lengths
414 uint32 dataSize = READ_BE_UINT32(infoHeader + MBI_DFLEN);
415 uint32 rsrcSize = READ_BE_UINT32(infoHeader + MBI_RFLEN);
416
417 uint32 dataSizePad = (((dataSize + 127) >> 7) << 7);
418 uint32 rsrcSizePad = (((rsrcSize + 127) >> 7) << 7);
419
420 // Length check
421 if (MBI_INFOHDR + dataSizePad + rsrcSizePad == (uint32)stream.size()) {
422 _resForkOffset = MBI_INFOHDR + dataSizePad;
423 _resForkSize = rsrcSize;
424 }
425 }
426
427 if (_resForkOffset < 0)
428 return false;
429
430 _mode = kResForkMacBinary;
431 return load(stream);
432 }
433
loadFromRawFork(SeekableReadStream & stream)434 bool MacResManager::loadFromRawFork(SeekableReadStream &stream) {
435 _mode = kResForkRaw;
436 _resForkOffset = 0;
437 _resForkSize = stream.size();
438 return load(stream);
439 }
440
load(SeekableReadStream & stream)441 bool MacResManager::load(SeekableReadStream &stream) {
442 if (_mode == kResForkNone)
443 return false;
444
445 stream.seek(_resForkOffset);
446
447 _dataOffset = stream.readUint32BE() + _resForkOffset;
448 _mapOffset = stream.readUint32BE() + _resForkOffset;
449 _dataLength = stream.readUint32BE();
450 _mapLength = stream.readUint32BE();
451
452 // do sanity check
453 if (stream.eos() || _dataOffset >= (uint32)stream.size() || _mapOffset >= (uint32)stream.size() ||
454 _dataLength + _mapLength > (uint32)stream.size()) {
455 _resForkOffset = -1;
456 _mode = kResForkNone;
457 return false;
458 }
459
460 debug(7, "got header: data %d [%d] map %d [%d]",
461 _dataOffset, _dataLength, _mapOffset, _mapLength);
462
463 _stream = &stream;
464
465 readMap();
466 return true;
467 }
468
getDataFork()469 SeekableReadStream *MacResManager::getDataFork() {
470 if (!_stream)
471 return nullptr;
472
473 if (_mode == kResForkMacBinary) {
474 _stream->seek(MBI_DFLEN);
475 uint32 dataSize = _stream->readUint32BE();
476 return new SeekableSubReadStream(_stream, MBI_INFOHDR, MBI_INFOHDR + dataSize);
477 }
478
479 File *file = new File();
480 if (file->open(_baseFileName))
481 return file;
482 delete file;
483
484 return nullptr;
485 }
486
getResIDArray(uint32 typeID)487 MacResIDArray MacResManager::getResIDArray(uint32 typeID) {
488 int typeNum = -1;
489 MacResIDArray res;
490
491 for (int i = 0; i < _resMap.numTypes; i++)
492 if (_resTypes[i].id == typeID) {
493 typeNum = i;
494 break;
495 }
496
497 if (typeNum == -1)
498 return res;
499
500 res.resize(_resTypes[typeNum].items);
501
502 for (int i = 0; i < _resTypes[typeNum].items; i++)
503 res[i] = _resLists[typeNum][i].id;
504
505 return res;
506 }
507
getResTagArray()508 MacResTagArray MacResManager::getResTagArray() {
509 MacResTagArray tagArray;
510
511 if (!hasResFork())
512 return tagArray;
513
514 tagArray.resize(_resMap.numTypes);
515
516 for (uint32 i = 0; i < _resMap.numTypes; i++)
517 tagArray[i] = _resTypes[i].id;
518
519 return tagArray;
520 }
521
getResName(uint32 typeID,uint16 resID) const522 String MacResManager::getResName(uint32 typeID, uint16 resID) const {
523 int typeNum = -1;
524
525 for (int i = 0; i < _resMap.numTypes; i++)
526 if (_resTypes[i].id == typeID) {
527 typeNum = i;
528 break;
529 }
530
531 if (typeNum == -1)
532 return "";
533
534 for (int i = 0; i < _resTypes[typeNum].items; i++)
535 if (_resLists[typeNum][i].id == resID)
536 return _resLists[typeNum][i].name;
537
538 return "";
539 }
540
getResource(uint32 typeID,uint16 resID)541 SeekableReadStream *MacResManager::getResource(uint32 typeID, uint16 resID) {
542 int typeNum = -1;
543 int resNum = -1;
544
545 for (int i = 0; i < _resMap.numTypes; i++)
546 if (_resTypes[i].id == typeID) {
547 typeNum = i;
548 break;
549 }
550
551 if (typeNum == -1)
552 return nullptr;
553
554 for (int i = 0; i < _resTypes[typeNum].items; i++)
555 if (_resLists[typeNum][i].id == resID) {
556 resNum = i;
557 break;
558 }
559
560 if (resNum == -1)
561 return nullptr;
562
563 _stream->seek(_dataOffset + _resLists[typeNum][resNum].dataOffset);
564 uint32 len = _stream->readUint32BE();
565
566 // Ignore resources with 0 length
567 if (!len)
568 return nullptr;
569
570 return _stream->readStream(len);
571 }
572
getResource(const String & fileName)573 SeekableReadStream *MacResManager::getResource(const String &fileName) {
574 for (uint32 i = 0; i < _resMap.numTypes; i++) {
575 for (uint32 j = 0; j < _resTypes[i].items; j++) {
576 if (_resLists[i][j].nameOffset != -1 && fileName.equalsIgnoreCase(_resLists[i][j].name)) {
577 _stream->seek(_dataOffset + _resLists[i][j].dataOffset);
578 uint32 len = _stream->readUint32BE();
579
580 // Ignore resources with 0 length
581 if (!len)
582 return nullptr;
583
584 return _stream->readStream(len);
585 }
586 }
587 }
588
589 return nullptr;
590 }
591
getResource(uint32 typeID,const String & fileName)592 SeekableReadStream *MacResManager::getResource(uint32 typeID, const String &fileName) {
593 for (uint32 i = 0; i < _resMap.numTypes; i++) {
594 if (_resTypes[i].id != typeID)
595 continue;
596
597 for (uint32 j = 0; j < _resTypes[i].items; j++) {
598 if (_resLists[i][j].nameOffset != -1 && fileName.equalsIgnoreCase(_resLists[i][j].name)) {
599 _stream->seek(_dataOffset + _resLists[i][j].dataOffset);
600 uint32 len = _stream->readUint32BE();
601
602 // Ignore resources with 0 length
603 if (!len)
604 return nullptr;
605
606 return _stream->readStream(len);
607 }
608 }
609 }
610
611 return nullptr;
612 }
613
readMap()614 void MacResManager::readMap() {
615 _stream->seek(_mapOffset + 22);
616
617 _resMap.resAttr = _stream->readUint16BE();
618 _resMap.typeOffset = _stream->readUint16BE();
619 _resMap.nameOffset = _stream->readUint16BE();
620 _resMap.numTypes = _stream->readUint16BE();
621 _resMap.numTypes++;
622
623 _stream->seek(_mapOffset + _resMap.typeOffset + 2);
624 _resTypes = new ResType[_resMap.numTypes];
625
626 for (int i = 0; i < _resMap.numTypes; i++) {
627 _resTypes[i].id = _stream->readUint32BE();
628 _resTypes[i].items = _stream->readUint16BE();
629 _resTypes[i].offset = _stream->readUint16BE();
630 _resTypes[i].items++;
631
632 debug(8, "resType: <%s> items: %d offset: %d (0x%x)", tag2str(_resTypes[i].id), _resTypes[i].items, _resTypes[i].offset, _resTypes[i].offset);
633 }
634
635 _resLists = new ResPtr[_resMap.numTypes];
636
637 for (int i = 0; i < _resMap.numTypes; i++) {
638 _resLists[i] = new Resource[_resTypes[i].items];
639 _stream->seek(_resTypes[i].offset + _mapOffset + _resMap.typeOffset);
640
641 for (int j = 0; j < _resTypes[i].items; j++) {
642 ResPtr resPtr = _resLists[i] + j;
643
644 resPtr->id = _stream->readUint16BE();
645 resPtr->nameOffset = _stream->readUint16BE();
646 resPtr->dataOffset = _stream->readUint32BE();
647 _stream->readUint32BE();
648 resPtr->name = nullptr;
649
650 resPtr->attr = resPtr->dataOffset >> 24;
651 resPtr->dataOffset &= 0xFFFFFF;
652 }
653
654 for (int j = 0; j < _resTypes[i].items; j++) {
655 if (_resLists[i][j].nameOffset != -1) {
656 _stream->seek(_resLists[i][j].nameOffset + _mapOffset + _resMap.nameOffset);
657
658 byte len = _stream->readByte();
659 _resLists[i][j].name = new char[len + 1];
660 _resLists[i][j].name[len] = 0;
661 _stream->read(_resLists[i][j].name, len);
662 }
663 }
664 }
665 }
666
constructAppleDoubleName(String name)667 String MacResManager::constructAppleDoubleName(String name) {
668 // Insert "._" before the last portion of a path name
669 for (int i = name.size() - 1; i >= 0; i--) {
670 if (i == 0) {
671 name.insertChar('_', 0);
672 name.insertChar('.', 0);
673 } else if (name[i] == '/') {
674 name.insertChar('_', i + 1);
675 name.insertChar('.', i + 1);
676 break;
677 }
678 }
679
680 return name;
681 }
682
disassembleAppleDoubleName(String name,bool * isAppleDouble)683 String MacResManager::disassembleAppleDoubleName(String name, bool *isAppleDouble) {
684 if (isAppleDouble) {
685 *isAppleDouble = false;
686 }
687
688 // Remove "._" before the last portion of a path name.
689 for (int i = name.size() - 1; i >= 0; --i) {
690 if (i == 0) {
691 if (name.size() > 2 && name[0] == '.' && name[1] == '_') {
692 name.erase(0, 2);
693 if (isAppleDouble) {
694 *isAppleDouble = true;
695 }
696 }
697 } else if (name[i] == '/') {
698 if ((uint)(i + 2) < name.size() && name[i + 1] == '.' && name[i + 2] == '_') {
699 name.erase(i + 1, 2);
700 if (isAppleDouble) {
701 *isAppleDouble = true;
702 }
703 }
704 break;
705 }
706 }
707
708 return name;
709 }
710
711 } // End of namespace Common
712