FlightGear next
fgrcc.cxx
Go to the documentation of this file.
1// -*- coding: utf-8 -*-
2//
3// fgrcc.cxx --- Simple resource compiler for FlightGear
4// Copyright (C) 2017 Florent Rougon
5//
6// This program is free software; you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation; either version 2 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along
17// with this program; if not, write to the Free Software Foundation, Inc.,
18// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20#include <ios> // std::basic_ios, std::streamsize...
21#include <string>
22#include <array>
23#include <tuple>
24#include <vector>
25#include <iostream> // std::ios_base, std::cerr, etc.
26#include <iomanip>
27#include <sstream>
28#include <unordered_set>
29#include <algorithm>
30#include <numeric> // std::accumulate()
31#include <functional>
32#include <type_traits> // std::underlying_type
33#include <stdexcept>
34#include <cstdlib>
35#include <cstddef> // std::size_t
36#include <clocale>
37#include <cstring>
38#include <cerrno>
39#include <cassert>
40
41#include <zlib.h> // Z_BEST_COMPRESSION
42
43#include <simgear/embedded_resources/EmbeddedResource.hxx>
44#include <simgear/io/iostreams/sgstream.hxx>
45#include <simgear/misc/argparse.hxx>
46#include <simgear/misc/sg_path.hxx>
47#include <simgear/misc/strutils.hxx>
48#include <simgear/sg_inlines.h>
49#include <simgear/structure/exception.hxx>
50#include <simgear/xml/easyxml.hxx>
51
52#include "fgrcc.hxx"
53
54using std::string;
55using std::vector;
56using std::cout;
57using std::cerr;
58using simgear::enumValue;
59
60// The name is still hard-coded essentially in the text for --help
61// (cf. showUsage()), because the formatting there depends on how long the
62// name is, and it makes said text more readable and maintainable.
63static const string PROGNAME = "fgrcc";
64
65// 'stuff' should insert UTF-8-encoded text into the stream
66#define LOG(stuff) do { cerr << PROGNAME << ": " << stuff << "\n"; } while(0)
67
68static string prettyPrintNbOfBytes(std::size_t nbBytes)
69{
70 std::ostringstream oss;
71
72 oss << std::fixed << std::setprecision(1);
73 if (nbBytes >= 1024*1024) {
74 oss << static_cast<float>(nbBytes) / (1024*1024) << " MiB";
75 } else if (nbBytes >= 1024) {
76 oss << static_cast<float>(nbBytes) / 1024 << " KiB";
77 } else {
78 oss << nbBytes << " byte" << ((nbBytes == 1) ? "" : "s");
79 }
80
81 return oss.str();
82}
83
84static SGPath assembleVirtualPath(string firstPart, const string& secondPart)
85{
86 if (!simgear::strutils::ends_with(firstPart, "/")) {
87 firstPart += '/';
88 }
89
90 assert( !(simgear::strutils::starts_with(secondPart, "/") ||
91 simgear::strutils::ends_with(secondPart, "/")) );
92 SGPath virtualPath = SGPath::fromUtf8(firstPart + secondPart);
93
94 return virtualPath;
95}
96
97// ***************************************************************************
98// * ResourceDeclaration *
99// ***************************************************************************
101 const SGPath& virtualPath_, const SGPath& realPath_,
102 const std::string& language_,
103 simgear::AbstractEmbeddedResource::CompressionType compressionType_)
104 : virtualPath(virtualPath_),
105 realPath(realPath_),
106 language(language_),
107 compressionType(compressionType_)
108{ }
109
111{
112 bool res;
113
114 switch (compressionType) {
115 case simgear::AbstractEmbeddedResource::CompressionType::ZLIB:
116 res = true;
117 break;
118 case simgear::AbstractEmbeddedResource::CompressionType::NONE:
119 res = false;
120 break;
121 default:
122 throw sg_exception("bug: unexpected compression type for an embedded "
123 "resource: " +
124 std::to_string(enumValue(compressionType)));
125 }
126
127 return res;
128}
129
130// ************************************************************************
131// * ResourceBuilderXMLVisitor *
132// ************************************************************************
133
134// Initialization of static members
135const std::array<string, 2> ResourceBuilderXMLVisitor::_tagTypeStr = {
136 {"start",
137 "end"}
138};
139const std::array<string, 5> ResourceBuilderXMLVisitor::_parserStateStr = {
140 {"before 'FGRCC' element",
141 "inside 'FGRCC' element",
142 "inside 'qresource' element",
143 "inside 'file' element",
144 "after 'FGRCC' element"
145 }
146};
147
149 : _rootDir(rootDir)
150{ }
151
152const vector<ResourceDeclaration>&
154{
155 return _resourceDeclarations;
156}
157
158// Static method
159bool
160ResourceBuilderXMLVisitor::readBoolean(const string& s)
161{
162 if (s == "yes" || s == "true" || s == "1") {
163 return true;
164 } else if (s == "no" || s == "false" || s == "0") {
165 return false;
166 } else {
167 throw sg_exception(
168 "invalid value for a boolean attribute: '" + s + "'. Authorized values "
169 "are 'yes', 'no', 'true', 'false', '1' and '0'.");
170 }
171}
172
173// Static method
174simgear::AbstractEmbeddedResource::CompressionType
175ResourceBuilderXMLVisitor::determineCompressionType(
176 const SGPath& resFilePath, const std::string& compression)
177{
178 const std::unordered_set<std::string> extsWithNoCompression = {
179 "png", "jpg", "jpeg", "gz", "bz2", "xz", "lzma", "zip" };
180 const std::string ext = resFilePath.lower_extension();
181 simgear::AbstractEmbeddedResource::CompressionType res;
182
183 if (compression == "none") {
184 res = simgear::AbstractEmbeddedResource::CompressionType::NONE;
185 } else if (compression == "zlib") {
186 res = simgear::AbstractEmbeddedResource::CompressionType::ZLIB;
187 } else if (compression == "auto") {
188 res = (extsWithNoCompression.count(ext) > 0) ?
189 simgear::AbstractEmbeddedResource::CompressionType::NONE :
190 simgear::AbstractEmbeddedResource::CompressionType::ZLIB;
191 } else {
192 throw sg_exception(
193 "invalid value for the 'compression' attribute: '" + compression + "'");
194 }
195
196 return res;
197}
198
199[[ noreturn ]] void
200ResourceBuilderXMLVisitor::unexpectedTagError(
201 XMLTagType tagType, const string& found, const string& expected)
202{
203 std::ostringstream oss;
204 string final = expected.empty() ? "" :
205 " (expected '" + expected + "' instead)";
206
207 savePosition();
208 oss << getPath() << ":" << getLine() << ":" << getColumn() <<
209 ": unexpected " << _tagTypeStr[enumValue(tagType)] << " tag: '" << found <<
210 "'" << final;
211
212 throw sg_exception(oss.str());
213}
214
215void
217 const XMLAttributes& atts)
218{
219 switch (_parserState) {
220 case ParserState::START:
221 if (!std::strcmp(name, "FGRCC")) {
222 _parserState = ParserState::INSIDE_FGRCC_ELT;
223 } else {
224 unexpectedTagError(XMLTagType::START, name, "FGRCC");
225 }
226 break;
227 case ParserState::INSIDE_FGRCC_ELT:
228 if (!std::strcmp(name, "qresource")) {
229 startQResourceElement(atts);
230 _parserState = ParserState::INSIDE_QRESOURCE_ELT;
231 } else {
232 unexpectedTagError(XMLTagType::START, name, "qresource");
233 }
234 break;
235 case ParserState::INSIDE_QRESOURCE_ELT:
236 if (!std::strcmp(name, "file")) {
237 startFileElement(atts);
238 _parserState = ParserState::INSIDE_FILE_ELT;
239 } else {
240 unexpectedTagError(XMLTagType::START, name, "file");
241 }
242 break;
243 case ParserState::INSIDE_FILE_ELT:
244 unexpectedTagError(XMLTagType::START, name); // throws an exception
245 case ParserState::END:
246 unexpectedTagError(XMLTagType::START, name); // throws an exception
247 default:
248 throw std::logic_error(
249 "unexpected state reached in resource file parser: " +
250 std::to_string(enumValue(_parserState)));
251 }
252}
253
254void
256{
257 switch (_parserState) {
258 case ParserState::START:
259 unexpectedTagError(XMLTagType::END, name); // throws an exception
260 case ParserState::INSIDE_FGRCC_ELT:
261 if (!std::strcmp(name, "FGRCC")) {
262 _parserState = ParserState::END;
263 } else {
264 unexpectedTagError(XMLTagType::END, name, "FGRCC");
265 }
266 break;
267 case ParserState::INSIDE_QRESOURCE_ELT:
268 if (!std::strcmp(name, "qresource")) {
269 _parserState = ParserState::INSIDE_FGRCC_ELT;
270 } else {
271 unexpectedTagError(XMLTagType::END, name, "qresource");
272 }
273 break;
274 case ParserState::INSIDE_FILE_ELT:
275 if (!std::strcmp(name, "file")) {
276 // First do some sanity checks, then assemble the resource virtual path
277 const auto throwError = [this]
278 (const string& contents, const string& message)
279 {
280 savePosition();
281 std::ostringstream oss;
282 oss << this->getPath() << ":" << this->getLine() << ":" <<
283 this->getColumn() << ": invalid contents for a <file> element " <<
284 message;
285 throw sg_format_exception(oss.str(), contents);
286 };
287
288 if (_resourceFile.empty()) {
289 throwError(_resourceFile, "(empty)");
290 } else if (simgear::strutils::ends_with(_resourceFile, "/")) {
291 throwError(_resourceFile,
292 "(ending with a '/'): '" + _resourceFile + "'");
293 }
294
295 const string secondPart = (_currentAlias.empty() ? _resourceFile :
296 _currentAlias);
297 // Make sure we don't get double slashes and similar problems
298 SGPath virtualPath = assembleVirtualPath(_currentPrefix, secondPart);
299
300 // We have to be careful here, because SGPath::append() and
301 // SGPath::operator/() don't behave in the expected way when the path is
302 // just '/' (they always insert a '/' before the second part, if it
303 // doesn't itself start with a '/').
304 SGPath p = _rootDir;
305 if (p.utf8Str() == "/") {
306 p = SGPath();
307 }
308 const SGPath realPath = p / _resourceFile;
309
310 const auto compressionType = determineCompressionType(
311 realPath, _currentCompressionTypeStr);
312 // Record what we've gathered for later processing (this way, we'll
313 // only start writing to the output stream if the input is entirely
314 // correct).
315 _resourceDeclarations.emplace_back(
316 virtualPath, realPath, _currentLanguage, compressionType);
317
318 _parserState = ParserState::INSIDE_QRESOURCE_ELT;
319 } else {
320 unexpectedTagError(XMLTagType::END, name, "file");
321 }
322 break;
323 case ParserState::END:
324 unexpectedTagError(XMLTagType::END, name); // throws an exception
325 default:
326 throw std::logic_error(
327 "unexpected state reached in resource file parser: " +
328 std::to_string(enumValue(_parserState)));
329 }
330}
331
332void
333ResourceBuilderXMLVisitor::startQResourceElement(const XMLAttributes &atts)
334{
335 const char *prefix = atts.getValue("prefix");
336 // Make a copy, as 'atts' is short-lived.
337 _currentPrefix = string(prefix ? prefix : "/");
338
339 // Make sure all virtual path prefixes are normalized
340 if (!simgear::strutils::starts_with(_currentPrefix, "/") ||
341 (_currentPrefix != "/" && simgear::strutils::ends_with(_currentPrefix,
342 "/"))) {
343 savePosition();
344 std::ostringstream oss;
345 oss << getPath() << ":" << getLine() << ": invalid 'prefix' attribute: '" <<
346 _currentPrefix << "' (must start with a '/' and not end with a '/', "
347 "unless equal to the one-char prefix '/')";
348 throw sg_format_exception(oss.str(), _currentPrefix);
349 }
350
351 const char *lang = atts.getValue("lang");
352 _currentLanguage = string(lang ? lang : "");
353}
354
355void
356ResourceBuilderXMLVisitor::startFileElement(const XMLAttributes &atts)
357{
358 const char *alias = atts.getValue("alias");
359
360 if (alias && !std::strcmp(alias, "")) {
361 std::ostringstream oss;
362 oss << getPath() << ":" << getLine() << ": invalid empty 'alias' attribute";
363 throw sg_format_exception(oss.str(), string(alias));
364 }
365
366 // cf. comment in startQResourceElement()
367 _currentAlias = string(alias ? alias : "");
368
369 const auto checkForError = [this]
370 (const string& valueToTest, const string& startingOrEnding,
371 std::function<bool(const string&, const string &)> testFunc)
372 {
373 if (testFunc(valueToTest, "/")) {
374 this->savePosition();
375 std::ostringstream oss;
376 oss << this->getPath() << ":" << this->getLine() <<
377 ": invalid 'alias' attribute " << startingOrEnding <<
378 " with a '/': '" << valueToTest << "'";
379 throw sg_format_exception(oss.str(), valueToTest);
380 }
381 };
382
383 checkForError(_currentAlias, "starting", simgear::strutils::starts_with);
384 checkForError(_currentAlias, "ending", simgear::strutils::ends_with);
385
386 // This attribute is not part of the v1.0 QRC format, it's a FlightGear
387 // extension.
388 const char *compress = atts.getValue("compression");
389 _currentCompressionTypeStr = string(compress ? compress : "auto");
390
391 // Start assembling a new file path (it may come in several chunks)
392 _resourceFile.clear();
393}
394
395void
396ResourceBuilderXMLVisitor::data(const char *s, int len)
397{
398 string chunk(s, len);
399
400 if (_parserState == ParserState::INSIDE_FILE_ELT) {
401 if (_resourceFile.empty() && simgear::strutils::starts_with(chunk, "/")) {
402 savePosition();
403 std::ostringstream oss;
404 oss << getPath() << ":" << getLine() << ":" << getColumn() <<
405 ": invalid <file> element (contents starting with a '/'): " << chunk <<
406 "...";
407 throw sg_format_exception(oss.str(), chunk);
408 }
409
410 _resourceFile += chunk;
411 } else if (chunk.find_first_not_of(" \t\n") != string::npos) {
412 // We are not inside a <file> element and we found character data that
413 // contains something different from spaces, tabs and newlines -> this is
414 // invalid.
415 std::ostringstream oss;
416
417 savePosition();
418 oss << getPath() << ":" << getLine() << ":" << getColumn() <<
419 ": unexpected character data " <<
420 _parserStateStr[enumValue(_parserState)] << ": '" << string(s, len) <<
421 "'";
422
423 throw sg_exception(oss.str());
424 }
425}
426
427void
428ResourceBuilderXMLVisitor::warning(const char *message, int line, int column)
429{
430 LOG("warning: " << getPath() << ": " << line << ":" << column << ": " <<
431 message);
432}
433
434void
435ResourceBuilderXMLVisitor::error(const char *message, int line, int column)
436{
437 std::ostringstream oss;
438 oss << getPath() << ": " << line << ":" << column << ": " << message;
439
440 throw sg_exception(oss.str());
441}
442
443// ************************************************************************
444// * Writing the generated code *
445// ************************************************************************
446
447CPPEncoder::CPPEncoder(std::istream& inputStream)
448 : _inputStream(inputStream)
449{ }
450
451// Static method
452[[ noreturn ]] void CPPEncoder::handleWriteError(int errorNumber)
453{
454 throw sg_exception(
455 "error while writing octal-encoded resource data: " +
456 simgear::strutils::error_string(errorNumber));
457}
458
459// Extract bytes from a stream, write them in lines of octal-encoded C++
460// character escapes. Return the number of bytes from the input stream that
461// have been encoded.
462std::size_t CPPEncoder::write(std::ostream& oStream)
463{
464 char buf[4096];
465 std::streamsize nbBytesRead;
466 // Lines will be at most 80 characters wide because of the '";' following
467 // the string literal contents.
468 const std::size_t maxColumns = 78;
469 std::size_t availableColumns = 0; // what remains to fill for current line
470 int savedErrno;
471 std::size_t payloadSize = 0;
472
473 do {
474 // std::ifstream::read() sets *both* the eofbit and failbit flags if EOF
475 // was reached before it could read the number of characters requested.
476 _inputStream.read(buf, sizeof(buf));
477 savedErrno = errno;
478 nbBytesRead = _inputStream.gcount();
479 auto charPtr = reinterpret_cast<const unsigned char*>(buf);
480
481 // Process what has been read (*even* if _inputStream.fail() is true)
482 for (std::streamsize remaining = nbBytesRead; remaining > 0; remaining--) {
483 std::ostringstream oss;
484 oss << "\\" << std::oct << static_cast<unsigned int>(*charPtr++);
485 string theCharLiteral = oss.str();
486
487 if (availableColumns < theCharLiteral.size()) {
488 if (!(oStream << "\\\n")) {
489 handleWriteError(errno);
490 }
491
492 availableColumns = maxColumns;
493 }
494
495 if (!(oStream << theCharLiteral)) {
496 handleWriteError(errno);
497 }
498 availableColumns -= theCharLiteral.size();
499 } // of for loop over the 'nbBytesRead' bytes
500
501 payloadSize += nbBytesRead;
502 } while (_inputStream);
503
504 if (_inputStream.bad()) {
505 throw sg_exception(
506 "error while reading from the possibly-compressed resource stream: " +
507 simgear::strutils::error_string(savedErrno));
508 }
509
510 return payloadSize;
511}
512
513// Weaker interface that might be convenient in some cases...
514std::ostream& operator<<(std::ostream& outputStream, CPPEncoder& cppEncoder)
515{
516 cppEncoder.write(outputStream);
517 return outputStream;
518}
519
520// ***************************************************************************
521// * ResourceCodeGenerator class *
522// ***************************************************************************
523
525 const vector<ResourceDeclaration>& resourceDeclarations,
526 std::ostream& outputStream,
527 const SGPath& outputCppFile,
528 const string& initFuncName,
529 const SGPath& outputHeaderFile,
530 const string& headerIdentifier,
531 std::size_t compInBufSize,
532 std::size_t compOutBufSize)
533 : _resDecl(resourceDeclarations),
534 _outputStream(outputStream),
535 _outputCppFile(outputCppFile),
536 _initFuncName(initFuncName),
537 _outputHeaderFile(outputHeaderFile),
538 _headerIdentifier(headerIdentifier),
539 _compInBufSize(compInBufSize),
540 _compOutBufSize(compOutBufSize),
541 _compressionInBuf(new char[_compInBufSize]),
542 _compressionOutBuf(new char[_compOutBufSize])
543{ }
544
545std::size_t ResourceCodeGenerator::writeEncodedResourceContents(
546 const ResourceDeclaration& resDecl) const
547{
548 std::unique_ptr<std::istream> iFileStream_p(
549 static_cast<std::istream *>(new sg_ifstream(resDecl.realPath)));
550
551 if (! *iFileStream_p) {
552 throw sg_exception("unable to open file '" + resDecl.realPath.utf8Str() +
553 "': " + simgear::strutils::error_string(errno));
554 }
555
556 std::unique_ptr<std::istream> iStream_p;
557
558 switch (resDecl.compressionType) {
559 case simgear::AbstractEmbeddedResource::CompressionType::ZLIB:
560 iStream_p.reset(
561 static_cast<std::istream *>(
562 new simgear::ZlibCompressorIStream(
563 std::move(iFileStream_p), resDecl.realPath, Z_BEST_COMPRESSION,
564 simgear::ZLibCompressionFormat::ZLIB,
565 simgear::ZLibMemoryStrategy::FAVOR_SPEED_OVER_MEMORY,
566 &_compressionInBuf[0], _compInBufSize,
567 &_compressionOutBuf[0], _compOutBufSize, /* putbackSize */ 0)));
568 break;
569 case simgear::AbstractEmbeddedResource::CompressionType::NONE:
570 iStream_p = std::move(iFileStream_p);
571 break;
572 default:
573 throw sg_exception("bug: unexpected compression type for an embedded "
574 "resource: " +
575 std::to_string(enumValue(resDecl.compressionType)));
576 }
577
578 // Throws in case of an error
579 return CPPEncoder(*iStream_p).write(_outputStream);
580}
581
582// Static method
583string ResourceCodeGenerator::resourceClass(
584 simgear::AbstractEmbeddedResource::CompressionType compressionType)
585{
586 string resClass;
587
588 switch (compressionType) {
589 case simgear::AbstractEmbeddedResource::CompressionType::ZLIB:
590 resClass = "ZlibEmbeddedResource";
591 break;
592 case simgear::AbstractEmbeddedResource::CompressionType::NONE:
593 resClass = "RawEmbeddedResource";
594 break;
595 default:
596 throw sg_exception("bug: unexpected compression type for an embedded "
597 "resource: "
598 + std::to_string(enumValue(compressionType)));
599 }
600
601 return resClass;
602}
603
604// Static method
605//
606// Encode an integral index in a way that can be safely used as part of a C++
607// variable name. This is base 26 written from right to left (most significant
608// digit last).
609string ResourceCodeGenerator::encodeResourceIndex(std::size_t index) {
610 string res;
611 std::size_t remainder;
612
613 if (index == 0) {
614 res = string("A"); // 0
615 }
616
617 while (index > 0) {
618 remainder = index % 26;
619 res += 'A' + remainder; // append a digit
620 index /= 26;
621 }
622
623 return res;
624}
625
627{
628 // This exception is not usable on all systems (cf.
629 // <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66145>), but this bug
630 // will eventually go away, and in the meantime, it's acceptable here that
631 // the exception can't be caught on buggy systems. Other alternatives are
632 // all worse IMHO.
633 _outputStream.exceptions(std::ios_base::failbit | std::ios_base::badbit);
634
635 string msg = (_outputCppFile.isNull()) ?
636 "writing C++ contents to the standard output" :
637 "writing C++ file: '" + _outputCppFile.utf8Str() + "'";
638 LOG(msg);
639
640 _outputStream << "\
641// -*- coding: utf-8 -*-\n\
642//\n\
643// File automatically generated by " << PROGNAME << ".\n \
644\n\
645#include <memory>\n\
646#include <utility>\n\
647\n\
648#include <simgear/io/iostreams/CharArrayStream.hxx>\n\
649#include <simgear/io/iostreams/zlibstream.hxx>\n\
650#include <simgear/embedded_resources/EmbeddedResource.hxx>\n\
651#include <simgear/embedded_resources/EmbeddedResourceManager.hxx>\n\
652\n\
653using std::unique_ptr;\n\
654using simgear::AbstractEmbeddedResource;\n\
655using simgear::RawEmbeddedResource;\n\
656using simgear::ZlibEmbeddedResource;\n\
657using simgear::EmbeddedResourceManager;\n";
658
659 // If the resource is compressed, this is the compressed size.
660 vector<std::size_t> resSizeInBytes;
661
662 for (vector<ResourceDeclaration>::size_type resNum = 0;
663 resNum < _resDecl.size(); resNum++) {
664 const auto& resDcl = _resDecl[resNum];
665 _outputStream << "\nstatic const char resource" <<
666 encodeResourceIndex(resNum) << "[] = \"";
667 resSizeInBytes.push_back(writeEncodedResourceContents(resDcl));
668 _outputStream << "\";\n";
669 }
670
671 _outputStream << "\n"
672 "void " << _initFuncName << "()\n"
673 "{\n" <<
674 ((_resDecl.empty()) ? " " : " const auto& resMgr = ") <<
675 "EmbeddedResourceManager::instance();\n";
676
677 for (vector<ResourceDeclaration>::size_type resNum = 0;
678 resNum < _resDecl.size(); resNum++) {
679 const auto& resDcl = _resDecl[resNum];
680 string resClass = resourceClass(resDcl.compressionType);
681 string encodedResNum = encodeResourceIndex(resNum);
682
683 _outputStream.flags(std::ios::dec);
684 _outputStream << "\n unique_ptr<const " << resClass << "> res" <<
685 encodedResNum << "(\n new " << resClass << "(";
686 std::ostringstream resConstructArgs;
687
688 switch (resDcl.compressionType) {
689 case simgear::AbstractEmbeddedResource::CompressionType::ZLIB:
690 resConstructArgs << "resource" << encodedResNum << ", " <<
691 resSizeInBytes[resNum] << ", " << resDcl.realPath.sizeInBytes();
692 break;
693 case simgear::AbstractEmbeddedResource::CompressionType::NONE:
694 resConstructArgs << "resource" << encodedResNum << ", " <<
695 resDcl.realPath.sizeInBytes();
696 break;
697 default:
698 throw sg_exception(
699 "bug: unexpected compression type for an embedded resource: " +
700 std::to_string(enumValue(resDcl.compressionType)));
701 }
702
703 // Use UTF-8 as the output encoding
704 _outputStream << resConstructArgs.str() <<
705 "));\n resMgr->addResource("
706 "\"" << simgear::strutils::escape(resDcl.virtualPath.utf8Str()) << "\", "
707 "std::move(" << "res" << encodedResNum << ")";
708
709 if (!resDcl.language.empty()) {
710 _outputStream << ", \"" << simgear::strutils::escape(resDcl.language) <<
711 "\"";
712 }
713
714 _outputStream << ");\n";
715
716 // Print a log message about the resource we just added
717 std::ostringstream oss;
718 oss << "added '" << resDcl.realPath.utf8Str() << "' (";
719
720 if (resDcl.isCompressed()) {
721 oss << prettyPrintNbOfBytes(resDcl.realPath.sizeInBytes()) <<
722 "; compressed: " << prettyPrintNbOfBytes(resSizeInBytes[resNum]) <<
723 ")";
724 } else {
725 oss << prettyPrintNbOfBytes(resSizeInBytes[resNum]) << ")";
726 }
727
728 LOG(oss.str());
729 }
730
731 _outputStream << "}\n";
732
733 // Print the total size of resources
734 std::size_t staticMemoryUsedByResources = std::accumulate(
735 resSizeInBytes.begin(), resSizeInBytes.end(), std::size_t(0));
736 LOG("static memory used by resources (total): " <<
737 prettyPrintNbOfBytes(staticMemoryUsedByResources));
738
739 if (!_outputHeaderFile.isNull()) {
741 }
742}
743
745{
746 assert(!_outputHeaderFile.isNull());
747 sg_ofstream outFile(_outputHeaderFile);
748
749 if (!outFile) {
750 throw sg_exception("unable to open output header file '" +
751 _outputHeaderFile.utf8Str() + "': " +
752 simgear::strutils::error_string(errno));
753 }
754
755 outFile.exceptions(std::ios_base::failbit | std::ios_base::badbit);
756
757 LOG("writing header file: '" << _outputHeaderFile.utf8Str() << "'");
758 outFile << "\
759// -*- coding: utf-8 -*-\n\
760//\n\
761// Header file automatically generated by " << PROGNAME << ".\n \
762\n\
763#ifndef " << _headerIdentifier << "\n\
764#define " << _headerIdentifier << "\n\
765\n\
766void " << _initFuncName << "();\n\
767\n\
768#endif // of " << _headerIdentifier << "\n";
769}
770
771// ************************************************************************
772// * Other functions *
773// ************************************************************************
774
775void showUsage(std::ostream& os) {
776 os << "Usage: " << PROGNAME << " [OPTION...] INFILE\n"
777 "\
778Compile resources declared in INFILE, into C++ code.\n\
779\n\
780INFILE should be a file in XML format declaring a set of resources. Each\n\
781resource has a contents that is initially read from a file, and a virtual\n\
782path that will be used for retrieval of the resource contents via the\n\
783EmbeddedResourceManager. The real path of a resource (that allows 'fgrcc' to\n\
784retrieve the resource data), its virtual path as well as other attributes\n\
785are all declared in INPUT.\n\
786\n\
787For each resource declared in INPUT, 'fgrcc' thus reads metadata (virtual\n\
788path, language attribute...) and contents from the associated file. Then, it\n\
789generates C++ code that can be used to register the resources with SimGear's\n\
790EmbeddedResourceManager, of which FlightGear has an instance. In this\n\
791generated C++ code, the contents of each resource is represented by a static\n\
792array of const char. It is compressed by default, except for a few file\n\
793extensions (png, jpg, jpeg, gz, bz2...).\n\
794\n\
795The EmbeddedResourceManager\n\
796---------------------------\n\
797\n\
798The EmbeddedResourceManager is a SimGear class that provides several ways to\n\
799access the contents of a given resource. The simplest way is to retrieve the\n\
800resource contents as an std::string (this works for all kinds of resources,\n\
801be they text or binary). This method may be undesirable for large resources\n\
802though[1], because std::string contents is stored in dynamically allocated\n\
803memory, and therefore the creation of an std::string instance to hold the\n\
804resource contents always makes a copy of this contents (with automatic, on\n\
805the fly decompression for compressed resources).\n\
806\n\
807 [1] Which may be undesirable per se anyway, since they have to be stored\n\
808 in static memory.\n\
809\n\
810The EmbeddedResourceManager class offers a few methods that are more\n\
811memory-friendly for large resources: by using SimGear classes such as\n\
812CharArrayIStream and ZlibDecompressorIStream, it gives zero-copy,\n\
813incremental access to resource contents, with transparent decompression in\n\
814the case of compressed resources. These two classes are derived from\n\
815std::istream, therefore this contents can be easily processed with standard\n\
816C++ techniques. For highest performance (using lower-level methods), the\n\
817EmbeddedResourceManager also provides access to resource data via an\n\
818std::streambuf interface, by means of classes such as ROCharArrayStreambuf\n\
819and ZlibDecompressorIStreambuf.\n\
820\n\
821Format of the resource declaration file\n\
822---------------------------------------\n\
823\n\
824The supported format for INFILE is a thin superset of the v1.0 QRC format\n\
825used by Qt (<http://doc.qt.io/qt-5/resources.html>). The differences with\n\
826this QRC format are:\n\
827\n\
828 1. The <!DOCTYPE RCC> declaration at the beginning should be omitted (or\n\
829 replaced with <!DOCTYPE FGRCC>, however such a DTD currently doesn't\n\
830 exist). I suggest to add an XML declaration instead, for instance:\n\
831\n\
832 <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
833\n\
834 2. <RCC> and </RCC> must be replaced with <FGRCC> and </FGRCC>,\n\
835 respectively.\n\
836\n\
837 3. The FGRCC format supports a 'compression' attribute for each 'file'\n\
838 element. At the time of this writing, the allowed values for this\n\
839 attribute are 'none', 'zlib' and 'auto'. When set to a value that is\n\
840 not 'auto', this attribute of course bypasses the algorithm for\n\
841 determining whether and how to compress a given resource (algorithm\n\
842 which relies on the file extension).\n\
843\n\
844 4. Resource paths (paths to the real files, not virtual paths) are\n\
845 interpreted relatively to the directory specified with the --root\n\
846 option. If this option is not passed to 'fgrcc', then the default root\n\
847 directory is the one containing INFILE, which matches the behavior of\n\
848 Qt's 'rcc' tool.\n\
849\n\
850Here follows a sample resource declaration file. In the comments, we use\n\
851$ROOT to represent the folder specified with --root (this $ROOT notation is\n\
852only a placeholder used to explain the concepts here, it is *not* syntax\n\
853understood by 'fgrcc'!).\n\
854\n\
855<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
856\n\
857<FGRCC version=\"1.0\">\n\
858 <qresource>\n\
859 <!-- The contents of '$ROOT/path/to/a/file' will be served by the\n\
860 EmbeddedResourceManager under the virtual path '/path/to/a/file'\n\
861 (anchored to '/' because we didn't define any prefix; see below).\n\
862 -->\n\
863 <file>path/to/a/file</file>\n\
864 <file compression=\"none\">another/file (won't be compressed)</file>\n\
865 <!-- This one will have the virtual path '/foobar/intro.txt'. -->\n\
866 <file alias=\"foobar/intro.txt\">yet another/file</file>\n\
867 </qresource>\n\
868\n\
869 <qresource prefix=\"/some/prefix\">\n\
870 <!-- The contents of '$ROOT/path/to/file1' will be served by the\n\
871 EmbeddedResourceManager under the virtual path\n\
872 '/some/prefix/path/to/file1'. -->\n\
873 <file>path/to/file1</file>\n\
874 <!-- The contents of '$ROOT/other/file' will be accessible through the\n\
875 virtual path '/some/prefix/my/alias'. -->\n\
876 <file alias=\"my/alias\">other/file</file>\n\
877 </qresource>\n\
878\n\
879 <qresource>\n\
880 <!-- Default version of a resource -->\n\
881 <file>some/file</file>\n\
882 </qresource>\n\
883\n\
884 <qresource lang=\"fr\">\n\
885 <!-- French version of the same resource -->\n\
886 <file alias=\"some/file\">path/to/french/version</file>\n\
887 </qresource>\n\
888\n\
889 <qresource lang=\"fr_FR\">\n\
890 <!-- Ditto, but more specialized: French from France -->\n\
891 <file alias=\"some/file\">path/to/french/from/France/version</file>\n\
892 </qresource>\n\
893\n\
894 <qresource lang=\"de\">\n\
895 <!-- German version of the same resource -->\n\
896 <file alias=\"some/file\">path/to/german/version</file>\n\
897 </qresource>\n\
898</FGRCC>\n\
899\n\
900Options supported by 'fgrcc'\n\
901----------------------------\n\
902\n\
903 --root=DIR Root directory used to interpret the real path of each\n\
904 declared resource (default: the directory containing\n\
905 INFILE)\n\
906 -o, --output-cpp-file=CPP_OUT\n\
907 File where the main C++ output is to be written. If not\n\
908 specified, or if CPP_OUT is '-', the standard output is\n\
909 used.\n\
910 --output-header-file=HPP_OUT\n\
911 File where to write C++ header code corresponding to the\n\
912 code in CPP_OUT (this declares the function whose name can\n\
913 be chosen with --init-func-name, see below).\n\
914 --output-header-identifier=IDENT\n\
915 To avoid recursive inclusion, C and C++ header files\n\
916 are typically wrapped in a construct such as:\n\
917\n\
918 #ifndef _SOME_IDENTIFIER\n\
919 #define _SOME_IDENTIFIER\n\
920\n\
921 [...]\n\
922\n\
923 #endif // _SOME_IDENTIFIER\n\
924\n\
925 This option allows one to choose the identifier used in\n\
926 HPP_OUT, when the --output-header-file option has been\n\
927 given.\n\
928 --init-func-name=FUNC\n\
929 Name of the function declared in HPP_OUT and defined in\n\
930 CPP_OUT, that registers all resources from INFILE with the\n\
931 EmbeddedResourceManager.\n\
932 --help Display this message and exit.\n";
933}
934
935enum class ActionAfterCommandLineParsing {
936 CONTINUE = 0,
937 EXIT
938};
939
940struct CmdLineParams {
941 SGPath rootDir;
942 SGPath inputFile;
943 SGPath outputCppFile;
944 SGPath outputHeaderFile;
945 string headerIdentifier; // UTF-8 encoding
946 string initFuncName; // UTF-8 encoding
947};
948
949std::tuple<ActionAfterCommandLineParsing, int, CmdLineParams>
950parseCommandLine(int argc, const char *const *argv)
951{
952 using simgear::argparse::OptionArgType;
953 std::tuple<ActionAfterCommandLineParsing, int, CmdLineParams> res;
954 ActionAfterCommandLineParsing& action = std::get<0>(res);
955 int& exitStatus = std::get<1>(res);
956 CmdLineParams& params = std::get<2>(res);
957
958 // Default value for the parameters
959 params.rootDir = SGPath();
960 params.outputCppFile = SGPath("-"); // standard output
961 params.outputHeaderFile = SGPath(); // write no header file
962 params.headerIdentifier = string();
963 params.initFuncName = string("initEmbeddedResources");
964
965 simgear::argparse::ArgumentParser parser;
966 parser.addOption("root", OptionArgType::MANDATORY_ARGUMENT, "", "--root");
967 parser.addOption("output cpp file", OptionArgType::MANDATORY_ARGUMENT,
968 "-o", "--output-cpp-file");
969 parser.addOption("output header file", OptionArgType::MANDATORY_ARGUMENT,
970 "", "--output-header-file");
971 parser.addOption("header identifier", OptionArgType::MANDATORY_ARGUMENT,
972 "", "--output-header-identifier");
973 parser.addOption("init func name", OptionArgType::MANDATORY_ARGUMENT,
974 "", "--init-func-name");
975 parser.addOption("help", OptionArgType::NO_ARGUMENT, "", "--help");
976
977 const auto parseArgsRes = parser.parseArgs(argc, argv);
978
979 for (const auto& opt: parseArgsRes.first) {
980 if (opt.id() == "root") {
981 params.rootDir = SGPath::fromUtf8(opt.value());
982 } else if (opt.id() == "output cpp file") {
983 params.outputCppFile = SGPath::fromUtf8(opt.value());
984 } else if (opt.id() == "output header file") {
985 params.outputHeaderFile = SGPath::fromUtf8(opt.value());
986 } else if (opt.id() == "header identifier") {
987 if (opt.value().empty()) {
988 LOG("invalid empty value for option '" << opt.passedAs() << "'");
989 action = ActionAfterCommandLineParsing::EXIT;
990 exitStatus = EXIT_FAILURE;
991 return res;
992 }
993 params.headerIdentifier = opt.value();
994 } else if (opt.id() == "init func name") {
995 params.initFuncName = opt.value();
996 } else if (opt.id() == "help") {
997 showUsage(cout);
998 action = ActionAfterCommandLineParsing::EXIT;
999 exitStatus = EXIT_SUCCESS;
1000 return res;
1001 } else {
1002 showUsage(cerr);
1003 action = ActionAfterCommandLineParsing::EXIT;
1004 exitStatus = EXIT_FAILURE;
1005 return res;
1006 }
1007 }
1008
1009 if (parseArgsRes.second.size() != 1) {
1010 showUsage(cerr);
1011 action = ActionAfterCommandLineParsing::EXIT;
1012 exitStatus = EXIT_FAILURE;
1013 return res;
1014 }
1015
1016 params.inputFile = SGPath::fromUtf8(parseArgsRes.second[0]);
1017 if (!params.inputFile.isFile()) {
1018 LOG("not an existing file: '" << params.inputFile.utf8Str() << "'");
1019 action = ActionAfterCommandLineParsing::EXIT;
1020 exitStatus = EXIT_FAILURE;
1021 return res;
1022 }
1023
1024 if (!params.outputHeaderFile.isNull() && params.headerIdentifier.empty()) {
1025 LOG("option --output-header-identifier must be passed when "
1026 "--output-header-file has been given");
1027 action = ActionAfterCommandLineParsing::EXIT;
1028 exitStatus = EXIT_FAILURE;
1029 return res;
1030 }
1031
1032 if (params.rootDir.isNull()) {
1033 params.rootDir = params.inputFile.dirPath(); // behavior of Qt's rcc
1034 }
1035
1036 if (!params.rootDir.isDir()) {
1037 LOG("not an existing directory: '" << params.rootDir.utf8Str() << "'");
1038 action = ActionAfterCommandLineParsing::EXIT;
1039 exitStatus = EXIT_FAILURE;
1040 return res;
1041 }
1042
1043 action = ActionAfterCommandLineParsing::CONTINUE;
1044 return res;
1045}
1046
1047int doTheWork(CmdLineParams params)
1048{
1049 std::streambuf *outputStreamBuf;
1050 bool outputToStdout = (params.outputCppFile.utf8Str() == "-");
1051 SGPath outputCppFile;
1052 sg_ofstream output;
1053
1054 if (outputToStdout) {
1055 // 'outputCppFile' is a null SGPath; this indicates to downstream code
1056 // that there is no file name/path for the generated .cxx contents.
1057 outputStreamBuf = cout.rdbuf();
1058 } else {
1059 outputCppFile = params.outputCppFile;
1060 output.open(outputCppFile);
1061
1062 if (!output) {
1063 LOG("unable to open file '" << outputCppFile.utf8Str() << "': " <<
1064 simgear::strutils::error_string(errno));
1065 return EXIT_FAILURE;
1066 }
1067
1068 outputStreamBuf = output.rdbuf();
1069 }
1070
1071 std::ostream outputStream(outputStreamBuf);
1072 ResourceBuilderXMLVisitor xmlVisitor(params.rootDir);
1073 readXML(params.inputFile, xmlVisitor);
1074 ResourceCodeGenerator codeGenerator(xmlVisitor.getResourceDeclarations(),
1075 outputStream, outputCppFile,
1076 params.initFuncName,
1077 params.outputHeaderFile,
1078 params.headerIdentifier);
1079 codeGenerator.writeCode();
1080
1081 return EXIT_SUCCESS;
1082}
1083
1084int main(int argc, char **argv)
1085{
1086 int exitStatus = EXIT_FAILURE;
1087
1088 std::setlocale(LC_ALL, "");
1089 std::setlocale(LC_NUMERIC, "C");
1090 std::setlocale(LC_COLLATE, "C");
1091 // We *might* want to call std::ios_base::sync_with_stdio(false) to maxmize
1092 // I/O performance, since we are neither using C's stdio stuff nor threads
1093 // in this program... but I haven't seen any evidence that it is needed.
1094
1095 try {
1096 ActionAfterCommandLineParsing whatToDo;
1097 CmdLineParams params;
1098 std::tie(whatToDo, exitStatus, params) = parseCommandLine(argc, argv);
1099
1100 if (whatToDo == ActionAfterCommandLineParsing::CONTINUE) {
1101 exitStatus = doTheWork(params);
1102 }
1103 } catch (const sg_exception &e) {
1104 // e.getFormattedMessage() contains the input file path specified to
1105 // readXML(const SGPath &path, XMLVisitor &visitor) in UTF-8 encoding.
1106 LOG(e.getFormattedMessage());
1107 LOG("aborting");
1108 } catch (const std::exception &e) {
1109 LOG(e.what());
1110 LOG("aborting");
1111 }
1112
1113 return exitStatus;
1114}
#define p(x)
CPPEncoder(std::istream &inputStream)
Definition fgrcc.cxx:447
virtual std::size_t write(std::ostream &oStream)
Definition fgrcc.cxx:462
ResourceBuilderXMLVisitor(const SGPath &rootDir)
Definition fgrcc.cxx:148
void endElement(const char *name) override
Definition fgrcc.cxx:255
const std::vector< ResourceDeclaration > & getResourceDeclarations() const
Definition fgrcc.cxx:153
void warning(const char *message, int line, int column) override
Definition fgrcc.cxx:428
void data(const char *s, int len) override
Definition fgrcc.cxx:396
void startElement(const char *name, const XMLAttributes &atts) override
Definition fgrcc.cxx:216
void writeCode() const
Definition fgrcc.cxx:626
ResourceCodeGenerator(const std::vector< ResourceDeclaration > &resourceDeclarations, std::ostream &outputStream, const SGPath &outputCppFile, const std::string &initFuncName, const SGPath &outputHeaderFile, const std::string &headerIdentifier, std::size_t inBufSize=262144, std::size_t outBufSize=242144)
Definition fgrcc.cxx:524
void writeHeaderFile() const
Definition fgrcc.cxx:744
const char * name
int main()
#define LOG(stuff)
Definition fgrcc.cxx:66
void showUsage(std::ostream &os)
Definition fgrcc.cxx:775
std::ostream & operator<<(std::ostream &outputStream, CPPEncoder &cppEncoder)
Definition fgrcc.cxx:514
static SGPath assembleVirtualPath(string firstPart, const string &secondPart)
Definition fgrcc.cxx:84
static const string PROGNAME
Definition fgrcc.cxx:63
static string prettyPrintNbOfBytes(std::size_t nbBytes)
Definition fgrcc.cxx:68
simgear::AbstractEmbeddedResource::CompressionType compressionType
Definition fgrcc.hxx:48
bool isCompressed() const
Definition fgrcc.cxx:110
std::string language
Definition fgrcc.hxx:47
ResourceDeclaration(const SGPath &virtualPath, const SGPath &realPath, const std::string &language, simgear::AbstractEmbeddedResource::CompressionType compressionType)
Definition fgrcc.cxx:100